@@ -416,7 +627,11 @@ function OnboardingPageContent() {
)
}
- const stepInfo = STEP_INFO[currentStep - 1]
+ const isConsultant = mode === 'consultant'
+ const stepInfoArr = isConsultant ? STEP_INFO_CONSULTANT : STEP_INFO
+ const stepInfo = stepInfoArr[currentStep - 1]
+ const showRoleChoice = mode === 'choice'
+ const showConsultantLanding = isConsultant && consultantLanding
const renderSteps = () => (
<>
@@ -475,10 +690,150 @@ function OnboardingPageContent() {
isSaving={isSaving}
/>
)}
-
>
)
+ // ── Role Choice Screen (Step 0) ──
+ if (showRoleChoice) {
+ return (
+
+ )
+ }
+
+ // ── Consultant Landing Screen ──
+ if (showConsultantLanding) {
+ return (
+
+ )
+ }
+
+ // ── Steps 1–4 (self or consultant adding company) ──
return (
{/* ── Branded Header ── */}
@@ -511,7 +866,7 @@ function OnboardingPageContent() {
- {STEP_INFO.map((_, i) => {
+ {stepInfoArr.map((_, i) => {
const num = i + 1
return (
({
),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
import { createClient } from '@/lib/supabase/server'
import { hasAiConsent, grantAiConsent, revokeAiConsent } from '@/lib/extensions/ai-consent'
import { GET, POST, DELETE } from '../route'
@@ -91,7 +96,7 @@ describe('POST /api/ai-consent', () => {
expect(status).toBe(200)
expect(body.data.consented).toBe(true)
- expect(mockGrantAiConsent).toHaveBeenCalledWith(expect.anything(), 'user-1', 'receipt-ocr')
+ expect(mockGrantAiConsent).toHaveBeenCalledWith(expect.anything(), 'user-1', 'company-1', 'receipt-ocr')
})
it('returns 400 for non-AI extension', async () => {
@@ -131,6 +136,6 @@ describe('DELETE /api/ai-consent', () => {
expect(status).toBe(200)
expect(body.data.consented).toBe(false)
- expect(mockRevokeAiConsent).toHaveBeenCalledWith(expect.anything(), 'user-1', 'ai-chat')
+ expect(mockRevokeAiConsent).toHaveBeenCalledWith(expect.anything(), 'user-1', 'company-1', 'ai-chat')
})
})
diff --git a/app/api/ai-consent/route.ts b/app/api/ai-consent/route.ts
index abed5871..9b61f50e 100644
--- a/app/api/ai-consent/route.ts
+++ b/app/api/ai-consent/route.ts
@@ -7,6 +7,7 @@ import {
revokeAiConsent,
isAiExtension,
} from '@/lib/extensions/ai-consent'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET() {
const supabase = await createClient()
@@ -16,9 +17,11 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const statuses: Record = {}
for (const ext of AI_EXTENSIONS) {
- statuses[ext] = await hasAiConsent(supabase, user.id, ext)
+ statuses[ext] = await hasAiConsent(supabase, companyId, ext)
}
return NextResponse.json({ data: statuses })
@@ -32,6 +35,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const body = await request.json()
const { extension_id } = body
@@ -42,7 +47,7 @@ export async function POST(request: Request) {
)
}
- await grantAiConsent(supabase, user.id, extension_id)
+ await grantAiConsent(supabase, user.id, companyId, extension_id)
return NextResponse.json({ data: { consented: true } })
}
@@ -54,6 +59,8 @@ export async function DELETE(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const body = await request.json()
const { extension_id } = body
@@ -64,6 +71,6 @@ export async function DELETE(request: Request) {
)
}
- await revokeAiConsent(supabase, user.id, extension_id)
+ await revokeAiConsent(supabase, user.id, companyId, extension_id)
return NextResponse.json({ data: { consented: false } })
}
diff --git a/app/api/audit-trail/__tests__/route.test.ts b/app/api/audit-trail/__tests__/route.test.ts
index 976e72d2..4fa9550b 100644
--- a/app/api/audit-trail/__tests__/route.test.ts
+++ b/app/api/audit-trail/__tests__/route.test.ts
@@ -9,6 +9,11 @@ vi.mock('@/lib/core/audit/audit-service', () => ({
getAuditLog: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
import { createClient } from '@/lib/supabase/server'
import { getAuditLog } from '@/lib/core/audit/audit-service'
import { GET } from '../route'
diff --git a/app/api/audit-trail/route.ts b/app/api/audit-trail/route.ts
index 90fa10bb..cf5c4b3a 100644
--- a/app/api/audit-trail/route.ts
+++ b/app/api/audit-trail/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { getAuditLog } from '@/lib/core/audit/audit-service'
import type { AuditAction } from '@/types'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -11,6 +12,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const filters = {
diff --git a/app/api/bookkeeping/account-totals/route.ts b/app/api/bookkeeping/account-totals/route.ts
index c13749b8..3540ea4a 100644
--- a/app/api/bookkeeping/account-totals/route.ts
+++ b/app/api/bookkeeping/account-totals/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -9,6 +10,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const from = searchParams.get('from')
const to = searchParams.get('to')
@@ -27,7 +30,7 @@ export async function GET(request: Request) {
let entriesQuery = supabase
.from('journal_entries')
.select('id, entry_date')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('status', 'posted')
if (dateFrom) {
diff --git a/app/api/bookkeeping/accounts/[number]/route.ts b/app/api/bookkeeping/accounts/[number]/route.ts
index 94847881..8f81f364 100644
--- a/app/api/bookkeeping/accounts/[number]/route.ts
+++ b/app/api/bookkeeping/accounts/[number]/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { validateBody } from '@/lib/api/validate'
import { UpdateAccountSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
export async function DELETE(
request: Request,
@@ -15,11 +16,13 @@ export async function DELETE(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch the account to check if it's a system account
const { data: account, error: fetchError } = await supabase
.from('chart_of_accounts')
.select('id, is_system_account')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('account_number', number)
.single()
@@ -51,7 +54,7 @@ export async function DELETE(
.from('chart_of_accounts')
.delete()
.eq('id', account.id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (deleteError) {
return NextResponse.json({ error: deleteError.message }, { status: 500 })
@@ -72,6 +75,8 @@ export async function PUT(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, UpdateAccountSchema)
if (!validation.success) return validation.response
const body = validation.data
@@ -79,7 +84,7 @@ export async function PUT(
const { data, error } = await supabase
.from('chart_of_accounts')
.update(body)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('account_number', number)
.select()
.single()
diff --git a/app/api/bookkeeping/accounts/activate/route.ts b/app/api/bookkeeping/accounts/activate/route.ts
index ee8781f6..69d70ac3 100644
--- a/app/api/bookkeeping/accounts/activate/route.ts
+++ b/app/api/bookkeeping/accounts/activate/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { getBASReference } from '@/lib/bookkeeping/bas-reference'
+import { requireCompanyId } from '@/lib/company/context'
/**
* POST /api/bookkeeping/accounts/activate
@@ -17,6 +18,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const body = await request.json()
const accountNumbers: string[] = body.account_numbers
@@ -28,7 +31,7 @@ export async function POST(request: Request) {
const { data: existing } = await supabase
.from('chart_of_accounts')
.select('account_number')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.in('account_number', accountNumbers)
const existingNumbers = new Set((existing || []).map((a) => a.account_number))
@@ -42,6 +45,7 @@ export async function POST(request: Request) {
return {
user_id: user.id,
+ company_id: companyId,
account_number: ref.account_number,
account_name: ref.account_name,
account_class: ref.account_class,
diff --git a/app/api/bookkeeping/accounts/reference/route.ts b/app/api/bookkeeping/accounts/reference/route.ts
index 1f01ff0b..e547a49b 100644
--- a/app/api/bookkeeping/accounts/reference/route.ts
+++ b/app/api/bookkeeping/accounts/reference/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { fetchAllRows } from '@/lib/supabase/fetch-all'
import { NextResponse } from 'next/server'
import { BAS_REFERENCE } from '@/lib/bookkeeping/bas-reference'
+import { requireCompanyId } from '@/lib/company/context'
/**
* GET /api/bookkeeping/accounts/reference
@@ -17,13 +18,15 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch user's chart of accounts (paginated to avoid 1000-row limit)
try {
const userAccounts = await fetchAllRows<{ account_number: string; is_active: boolean; is_system_account: boolean }>(({ from, to }) =>
supabase
.from('chart_of_accounts')
.select('account_number, is_active, is_system_account')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.range(from, to)
)
diff --git a/app/api/bookkeeping/accounts/route.ts b/app/api/bookkeeping/accounts/route.ts
index 4f00e95f..3a174184 100644
--- a/app/api/bookkeeping/accounts/route.ts
+++ b/app/api/bookkeeping/accounts/route.ts
@@ -3,6 +3,7 @@ import { fetchAllRows } from '@/lib/supabase/fetch-all'
import { NextResponse } from 'next/server'
import { validateBody } from '@/lib/api/validate'
import { CreateAccountSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -12,6 +13,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const accountClass = searchParams.get('class')
const activeOnly = searchParams.get('active') !== 'false'
@@ -21,7 +24,7 @@ export async function GET(request: Request) {
let query = supabase
.from('chart_of_accounts')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.order('sort_order')
if (activeOnly) {
@@ -53,10 +56,13 @@ export async function POST(request: Request) {
if (!validation.success) return validation.response
const body = validation.data
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('chart_of_accounts')
.insert({
user_id: user.id,
+ company_id: companyId,
account_number: body.account_number,
account_name: body.account_name,
account_class: parseInt(body.account_number[0]),
diff --git a/app/api/bookkeeping/fiscal-periods/[id]/close/route.ts b/app/api/bookkeeping/fiscal-periods/[id]/close/route.ts
index 2b39890e..b9cff5ec 100644
--- a/app/api/bookkeeping/fiscal-periods/[id]/close/route.ts
+++ b/app/api/bookkeeping/fiscal-periods/[id]/close/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { closePeriod } from '@/lib/core/bookkeeping/period-service'
+import { requireCompanyId } from '@/lib/company/context'
export async function POST(
request: Request,
@@ -14,8 +15,10 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
try {
- const period = await closePeriod(supabase, user.id, id)
+ const period = await closePeriod(supabase, companyId, user.id, id)
return NextResponse.json({ data: period })
} catch (err) {
return NextResponse.json(
diff --git a/app/api/bookkeeping/fiscal-periods/[id]/currency-revaluation/route.ts b/app/api/bookkeeping/fiscal-periods/[id]/currency-revaluation/route.ts
index 7b98895c..71b0b98a 100644
--- a/app/api/bookkeeping/fiscal-periods/[id]/currency-revaluation/route.ts
+++ b/app/api/bookkeeping/fiscal-periods/[id]/currency-revaluation/route.ts
@@ -4,6 +4,7 @@ import {
previewCurrencyRevaluation,
executeCurrencyRevaluation,
} from '@/lib/bookkeeping/currency-revaluation'
+import { requireCompanyId } from '@/lib/company/context'
/**
* GET: Preview currency revaluation for a fiscal period
@@ -20,20 +21,22 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
try {
// Fetch period to get closing date
const { data: period, error: periodError } = await supabase
.from('fiscal_periods')
.select('*')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (periodError || !period) {
return NextResponse.json({ error: 'Fiscal period not found' }, { status: 404 })
}
- const preview = await previewCurrencyRevaluation(supabase, user.id, period.period_end)
+ const preview = await previewCurrencyRevaluation(supabase, companyId, period.period_end)
return NextResponse.json({ data: preview })
} catch (err) {
return NextResponse.json(
@@ -58,13 +61,15 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
try {
// Fetch period to get closing date
const { data: period, error: periodError } = await supabase
.from('fiscal_periods')
.select('*')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (periodError || !period) {
@@ -75,7 +80,7 @@ export async function POST(
return NextResponse.json({ error: 'Period is already closed' }, { status: 400 })
}
- const result = await executeCurrencyRevaluation(supabase, user.id, period.period_end, id)
+ const result = await executeCurrencyRevaluation(supabase, companyId, period.period_end, id, user.id)
if (!result) {
return NextResponse.json({ data: null, message: 'No foreign currency items to revalue' })
diff --git a/app/api/bookkeeping/fiscal-periods/[id]/lock/route.ts b/app/api/bookkeeping/fiscal-periods/[id]/lock/route.ts
index 80a0eb71..3512ed48 100644
--- a/app/api/bookkeeping/fiscal-periods/[id]/lock/route.ts
+++ b/app/api/bookkeeping/fiscal-periods/[id]/lock/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { lockPeriod } from '@/lib/core/bookkeeping/period-service'
+import { requireCompanyId } from '@/lib/company/context'
export async function POST(
request: Request,
@@ -14,8 +15,10 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
try {
- const period = await lockPeriod(supabase, user.id, id)
+ const period = await lockPeriod(supabase, companyId, user.id, id)
return NextResponse.json({ data: period })
} catch (err) {
return NextResponse.json(
diff --git a/app/api/bookkeeping/fiscal-periods/[id]/year-end/route.ts b/app/api/bookkeeping/fiscal-periods/[id]/year-end/route.ts
index 0f6e47c5..8f6660a7 100644
--- a/app/api/bookkeeping/fiscal-periods/[id]/year-end/route.ts
+++ b/app/api/bookkeeping/fiscal-periods/[id]/year-end/route.ts
@@ -5,6 +5,7 @@ import {
previewYearEndClosing,
executeYearEndClosing,
} from '@/lib/core/bookkeeping/year-end-service'
+import { requireCompanyId } from '@/lib/company/context'
/**
* GET: Validate readiness and preview year-end closing
@@ -21,10 +22,12 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
try {
const [validation, preview] = await Promise.all([
- validateYearEndReadiness(supabase, user.id, id),
- previewYearEndClosing(supabase, user.id, id),
+ validateYearEndReadiness(supabase, companyId, user.id, id),
+ previewYearEndClosing(supabase, companyId, user.id, id),
])
return NextResponse.json({ data: { validation, preview } })
@@ -51,8 +54,10 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
try {
- const result = await executeYearEndClosing(supabase, user.id, id)
+ const result = await executeYearEndClosing(supabase, companyId, user.id, id)
return NextResponse.json({ data: result })
} catch (err) {
return NextResponse.json(
diff --git a/app/api/bookkeeping/fiscal-periods/route.ts b/app/api/bookkeeping/fiscal-periods/route.ts
index 90a69b6c..1ae213a7 100644
--- a/app/api/bookkeeping/fiscal-periods/route.ts
+++ b/app/api/bookkeeping/fiscal-periods/route.ts
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server'
import { validatePeriodDuration } from '@/lib/bookkeeping/validate-period-duration'
import { validateBody } from '@/lib/api/validate'
import { CreateFiscalPeriodSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET() {
const supabase = await createClient()
@@ -12,10 +13,12 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('fiscal_periods')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.order('period_start', { ascending: false })
if (error) {
@@ -33,6 +36,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, CreateFiscalPeriodSchema)
if (!validation.success) return validation.response
const body = validation.data
@@ -47,7 +52,7 @@ export async function POST(request: Request) {
const { data: overlapping } = await supabase
.from('fiscal_periods')
.select('id, name')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.lte('period_start', body.period_end)
.gte('period_end', body.period_start)
.limit(1)
@@ -63,6 +68,7 @@ export async function POST(request: Request) {
.from('fiscal_periods')
.insert({
user_id: user.id,
+ company_id: companyId,
name: body.name,
period_start: body.period_start,
period_end: body.period_end,
diff --git a/app/api/bookkeeping/journal-entries/[id]/chain/__tests__/route.test.ts b/app/api/bookkeeping/journal-entries/[id]/chain/__tests__/route.test.ts
index bbc619bb..93924848 100644
--- a/app/api/bookkeeping/journal-entries/[id]/chain/__tests__/route.test.ts
+++ b/app/api/bookkeeping/journal-entries/[id]/chain/__tests__/route.test.ts
@@ -11,6 +11,11 @@ vi.mock('@/lib/supabase/server', () => ({
createClient: () => mockCreateClient(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
import { GET } from '../route'
function buildMockSupabase({
diff --git a/app/api/bookkeeping/journal-entries/[id]/chain/route.ts b/app/api/bookkeeping/journal-entries/[id]/chain/route.ts
index 9a1106af..21001c9f 100644
--- a/app/api/bookkeeping/journal-entries/[id]/chain/route.ts
+++ b/app/api/bookkeeping/journal-entries/[id]/chain/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(
request: Request,
@@ -13,12 +14,14 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch the requested entry with lines
const { data: entry, error } = await supabase
.from('journal_entries')
.select('*, lines:journal_entry_lines(*)')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (error || !entry) {
@@ -38,7 +41,7 @@ export async function GET(
const { data: referencing } = await supabase
.from('journal_entries')
.select('id')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.or(`reverses_id.eq.${id},reversed_by_id.eq.${id},correction_of_id.eq.${id}`)
if (referencing) {
@@ -57,7 +60,7 @@ export async function GET(
const { data: batchEntries } = await supabase
.from('journal_entries')
.select('id, reverses_id, reversed_by_id, correction_of_id')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.in('id', batch)
if (!batchEntries) continue
@@ -81,7 +84,7 @@ export async function GET(
const { data: refs } = await supabase
.from('journal_entries')
.select('id')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.or(batchOr)
if (refs) {
@@ -99,7 +102,7 @@ export async function GET(
const { data: chainEntries } = await supabase
.from('journal_entries')
.select('*, lines:journal_entry_lines(*)')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.in('id', chainIds)
.order('created_at', { ascending: true })
diff --git a/app/api/bookkeeping/journal-entries/[id]/correct/__tests__/route.test.ts b/app/api/bookkeeping/journal-entries/[id]/correct/__tests__/route.test.ts
index 09dff667..d28af8a0 100644
--- a/app/api/bookkeeping/journal-entries/[id]/correct/__tests__/route.test.ts
+++ b/app/api/bookkeeping/journal-entries/[id]/correct/__tests__/route.test.ts
@@ -15,6 +15,10 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
const mockCorrectEntry = vi.fn()
vi.mock('@/lib/core/bookkeeping/storno-service', () => ({
correctEntry: (...args: unknown[]) => mockCorrectEntry(...args),
@@ -100,7 +104,7 @@ describe('POST /api/bookkeeping/journal-entries/[id]/correct', () => {
expect(status).toBe(200)
expect(body.data.reversal).toEqual(reversal)
expect(body.data.corrected).toEqual(corrected)
- expect(mockCorrectEntry).toHaveBeenCalledWith(expect.anything(), 'user-1', 'entry-1', lines)
+ expect(mockCorrectEntry).toHaveBeenCalledWith(expect.anything(), 'company-1', 'user-1', 'entry-1', lines)
})
it('returns 400 when correctEntry throws for unbalanced lines', async () => {
diff --git a/app/api/bookkeeping/journal-entries/[id]/correct/route.ts b/app/api/bookkeeping/journal-entries/[id]/correct/route.ts
index 2e58eb13..79823ff1 100644
--- a/app/api/bookkeeping/journal-entries/[id]/correct/route.ts
+++ b/app/api/bookkeeping/journal-entries/[id]/correct/route.ts
@@ -4,6 +4,7 @@ import { correctEntry } from '@/lib/core/bookkeeping/storno-service'
import { ensureInitialized } from '@/lib/init'
import { validateBody } from '@/lib/api/validate'
import { CorrectJournalEntrySchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
ensureInitialized()
@@ -19,12 +20,14 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, CorrectJournalEntrySchema)
if (!validation.success) return validation.response
const body = validation.data
try {
- const result = await correctEntry(supabase, user.id, id, body.lines)
+ const result = await correctEntry(supabase, companyId, user.id, id, body.lines)
return NextResponse.json({ data: result })
} catch (err) {
return NextResponse.json(
diff --git a/app/api/bookkeeping/journal-entries/[id]/reverse/__tests__/route.test.ts b/app/api/bookkeeping/journal-entries/[id]/reverse/__tests__/route.test.ts
index e1a9bd03..2c61ddfc 100644
--- a/app/api/bookkeeping/journal-entries/[id]/reverse/__tests__/route.test.ts
+++ b/app/api/bookkeeping/journal-entries/[id]/reverse/__tests__/route.test.ts
@@ -16,6 +16,10 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
const mockReverseEntry = vi.fn()
vi.mock('@/lib/bookkeeping/engine', () => ({
reverseEntry: (...args: unknown[]) => mockReverseEntry(...args),
@@ -64,7 +68,7 @@ describe('POST /api/bookkeeping/journal-entries/[id]/reverse', () => {
expect(status).toBe(200)
expect(body.data).toEqual(reversalEntry)
- expect(mockReverseEntry).toHaveBeenCalledWith(expect.anything(), 'user-1', 'entry-1')
+ expect(mockReverseEntry).toHaveBeenCalledWith(expect.anything(), 'company-1', 'user-1', 'entry-1')
})
it('returns 400 when engine throws', async () => {
diff --git a/app/api/bookkeeping/journal-entries/[id]/reverse/route.ts b/app/api/bookkeeping/journal-entries/[id]/reverse/route.ts
index eca91f23..b243739b 100644
--- a/app/api/bookkeeping/journal-entries/[id]/reverse/route.ts
+++ b/app/api/bookkeeping/journal-entries/[id]/reverse/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { reverseEntry } from '@/lib/bookkeeping/engine'
import { ensureInitialized } from '@/lib/init'
+import { requireCompanyId } from '@/lib/company/context'
ensureInitialized()
@@ -17,8 +18,10 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
try {
- const reversalEntry = await reverseEntry(supabase, user.id, id)
+ const reversalEntry = await reverseEntry(supabase, companyId, user.id, id)
return NextResponse.json({ data: reversalEntry })
} catch (err) {
return NextResponse.json(
diff --git a/app/api/bookkeeping/journal-entries/[id]/route.ts b/app/api/bookkeeping/journal-entries/[id]/route.ts
index 5a0b9282..5b579465 100644
--- a/app/api/bookkeeping/journal-entries/[id]/route.ts
+++ b/app/api/bookkeeping/journal-entries/[id]/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(
request: Request,
@@ -13,11 +14,13 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('journal_entries')
.select('*, lines:journal_entry_lines(*)')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (error) {
diff --git a/app/api/bookkeeping/journal-entries/__tests__/route.test.ts b/app/api/bookkeeping/journal-entries/__tests__/route.test.ts
index 1a0a34c7..9c8ed521 100644
--- a/app/api/bookkeeping/journal-entries/__tests__/route.test.ts
+++ b/app/api/bookkeeping/journal-entries/__tests__/route.test.ts
@@ -16,6 +16,10 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
const mockCreateJournalEntry = vi.fn()
vi.mock('@/lib/bookkeeping/engine', () => ({
createJournalEntry: (...args: unknown[]) => mockCreateJournalEntry(...args),
@@ -137,7 +141,7 @@ describe('POST /api/bookkeeping/journal-entries', () => {
expect(status).toBe(200)
expect(body.data).toEqual(entry)
- expect(mockCreateJournalEntry).toHaveBeenCalledWith(expect.anything(), 'user-1', input)
+ expect(mockCreateJournalEntry).toHaveBeenCalledWith(expect.anything(), 'company-1', 'user-1', input)
})
it('returns 400 when engine throws', async () => {
diff --git a/app/api/bookkeeping/journal-entries/route.ts b/app/api/bookkeeping/journal-entries/route.ts
index 9bdaae1b..7b9747c0 100644
--- a/app/api/bookkeeping/journal-entries/route.ts
+++ b/app/api/bookkeeping/journal-entries/route.ts
@@ -4,6 +4,7 @@ import { createJournalEntry } from '@/lib/bookkeeping/engine'
import { ensureInitialized } from '@/lib/init'
import { validateBody } from '@/lib/api/validate'
import { CreateJournalEntrySchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
ensureInitialized()
@@ -15,6 +16,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
const status = searchParams.get('status')
@@ -29,7 +32,7 @@ export async function GET(request: Request) {
let query = supabase
.from('journal_entries')
.select('*, lines:journal_entry_lines(*)', { count: 'exact' })
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (sortDate === 'asc' || sortDate === 'desc') {
query = query
@@ -76,12 +79,14 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, CreateJournalEntrySchema)
if (!validation.success) return validation.response
const body = validation.data
try {
- const entry = await createJournalEntry(supabase, user.id, body)
+ const entry = await createJournalEntry(supabase, companyId, user.id, body)
return NextResponse.json({ data: entry })
} catch (err) {
return NextResponse.json(
diff --git a/app/api/bookkeeping/mapping-rules/evaluate/route.ts b/app/api/bookkeeping/mapping-rules/evaluate/route.ts
index 78f9bb65..6719e602 100644
--- a/app/api/bookkeeping/mapping-rules/evaluate/route.ts
+++ b/app/api/bookkeeping/mapping-rules/evaluate/route.ts
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server'
import { evaluateMappingRules } from '@/lib/bookkeeping/mapping-engine'
import { validateBody } from '@/lib/api/validate'
import { EvaluateMappingRulesSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
import type { Transaction } from '@/types'
export async function POST(request: Request) {
@@ -13,6 +14,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, EvaluateMappingRulesSchema)
if (!validation.success) return validation.response
const body = validation.data
@@ -25,7 +28,7 @@ export async function POST(request: Request) {
.from('transactions')
.select('*')
.eq('id', body.transaction_id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (error || !data) {
@@ -38,7 +41,7 @@ export async function POST(request: Request) {
}
try {
- const result = await evaluateMappingRules(supabase, user.id, transaction)
+ const result = await evaluateMappingRules(supabase, companyId, transaction)
return NextResponse.json({ data: result })
} catch (err) {
return NextResponse.json(
diff --git a/app/api/bookkeeping/mapping-rules/route.ts b/app/api/bookkeeping/mapping-rules/route.ts
index 80624a6d..5d44ed2d 100644
--- a/app/api/bookkeeping/mapping-rules/route.ts
+++ b/app/api/bookkeeping/mapping-rules/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { validateBody } from '@/lib/api/validate'
import { CreateMappingRuleSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET() {
const supabase = await createClient()
@@ -11,10 +12,12 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('mapping_rules')
.select('*')
- .or(`user_id.eq.${user.id},user_id.is.null`)
+ .or(`company_id.eq.${companyId},company_id.is.null`)
.eq('is_active', true)
.order('priority')
@@ -33,6 +36,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const result = await validateBody(request, CreateMappingRuleSchema)
if (!result.success) return result.response
const body = result.data
@@ -41,6 +46,7 @@ export async function POST(request: Request) {
.from('mapping_rules')
.insert({
user_id: user.id,
+ company_id: companyId,
rule_name: body.rule_name,
rule_type: body.rule_type,
priority: body.priority || 10,
diff --git a/app/api/calendar/feed/[token]/route.ts b/app/api/calendar/feed/[token]/route.ts
index 46e60c64..e1089491 100644
--- a/app/api/calendar/feed/[token]/route.ts
+++ b/app/api/calendar/feed/[token]/route.ts
@@ -101,7 +101,7 @@ export async function GET(
? supabase
.from('deadlines')
.select('*')
- .eq('user_id', feed.user_id)
+ .eq('company_id', feed.company_id)
.gte('due_date', startStr)
.lte('due_date', endStr)
.order('due_date')
@@ -112,7 +112,7 @@ export async function GET(
? supabase
.from('invoices')
.select('*, customer:customers(*)')
- .eq('user_id', feed.user_id)
+ .eq('company_id', feed.company_id)
.gte('due_date', startStr)
.lte('due_date', endStr)
.order('due_date')
diff --git a/app/api/calendar/feed/route.ts b/app/api/calendar/feed/route.ts
index 644edbe0..41b8ab55 100644
--- a/app/api/calendar/feed/route.ts
+++ b/app/api/calendar/feed/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
import type { UpdateCalendarFeedInput } from '@/types'
/**
@@ -15,10 +16,12 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data: feed, error } = await supabase
.from('calendar_feeds')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (error && error.code !== 'PGRST116') {
@@ -57,11 +60,13 @@ export async function POST() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Check if feed already exists
const { data: existingFeed } = await supabase
.from('calendar_feeds')
.select('id')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (existingFeed) {
@@ -76,6 +81,7 @@ export async function POST() {
.from('calendar_feeds')
.insert({
user_id: user.id,
+ company_id: companyId,
is_active: true,
include_tax_deadlines: true,
include_invoices: true,
@@ -111,12 +117,14 @@ export async function PUT(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const body: UpdateCalendarFeedInput = await request.json()
const { data: feed, error } = await supabase
.from('calendar_feeds')
.update(body)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.select()
.single()
@@ -148,6 +156,8 @@ export async function DELETE() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Generate a new token by updating with a new UUID
const { data: feed, error } = await supabase
.from('calendar_feeds')
@@ -156,7 +166,7 @@ export async function DELETE() {
access_count: 0,
last_accessed_at: null,
})
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.select()
.single()
diff --git a/app/api/company/members/[id]/route.ts b/app/api/company/members/[id]/route.ts
new file mode 100644
index 00000000..fdb03257
--- /dev/null
+++ b/app/api/company/members/[id]/route.ts
@@ -0,0 +1,86 @@
+import { createClient, createServiceClient } from '@/lib/supabase/server'
+import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
+
+/**
+ * DELETE /api/company/members/[id]
+ * Remove a member from the current company.
+ * Only company owners and admins can remove members.
+ * Cannot remove team-sourced members (they must be removed from the team).
+ */
+export async function DELETE(
+ _request: Request,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ const supabase = await createClient()
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+
+ const companyId = await requireCompanyId(supabase, user.id)
+ const { id: memberId } = await params
+ const serviceClient = await createServiceClient()
+
+ // Check caller has permission
+ const { data: callerMembership } = await serviceClient
+ .from('company_members')
+ .select('role')
+ .eq('company_id', companyId)
+ .eq('user_id', user.id)
+ .single()
+
+ if (!callerMembership || !['owner', 'admin'].includes(callerMembership.role)) {
+ return NextResponse.json({ error: 'Behörighet saknas.' }, { status: 403 })
+ }
+
+ // Look up the member (source column may not exist if migration not yet applied)
+ let member: { id: string; user_id: string; role: string; source?: string } | null = null
+
+ const { data: memberWithSource } = await serviceClient
+ .from('company_members')
+ .select('id, user_id, role, source')
+ .eq('id', memberId)
+ .eq('company_id', companyId)
+ .single()
+
+ if (memberWithSource) {
+ member = memberWithSource
+ } else {
+ const { data: memberFallback } = await serviceClient
+ .from('company_members')
+ .select('id, user_id, role')
+ .eq('id', memberId)
+ .eq('company_id', companyId)
+ .single()
+ member = memberFallback ? { ...memberFallback, source: 'direct' } : null
+ }
+
+ if (!member) {
+ return NextResponse.json({ error: 'Medlem hittades inte.' }, { status: 404 })
+ }
+
+ if (member.user_id === user.id) {
+ return NextResponse.json({ error: 'Du kan inte ta bort dig själv.' }, { status: 400 })
+ }
+
+ if (member.role === 'owner') {
+ return NextResponse.json({ error: 'Ägaren kan inte tas bort.' }, { status: 400 })
+ }
+
+ if (member.source === 'team') {
+ return NextResponse.json({
+ error: 'Denna medlem läggs till via teamet. Ta bort från teamet istället.',
+ }, { status: 400 })
+ }
+
+ const { error } = await serviceClient
+ .from('company_members')
+ .delete()
+ .eq('id', memberId)
+ .eq('company_id', companyId)
+
+ if (error) {
+ return NextResponse.json({ error: 'Kunde inte ta bort medlem.' }, { status: 500 })
+ }
+
+ return NextResponse.json({ data: { removed: memberId } })
+}
diff --git a/app/api/company/members/invite/[id]/route.ts b/app/api/company/members/invite/[id]/route.ts
new file mode 100644
index 00000000..d58691ff
--- /dev/null
+++ b/app/api/company/members/invite/[id]/route.ts
@@ -0,0 +1,62 @@
+import { createClient, createServiceClient } from '@/lib/supabase/server'
+import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
+
+/**
+ * DELETE /api/company/members/invite/[id]
+ * Revoke a pending company invitation.
+ * Only company owners and admins can revoke.
+ */
+export async function DELETE(
+ _request: Request,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ const supabase = await createClient()
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+
+ const companyId = await requireCompanyId(supabase, user.id)
+ const { id: inviteId } = await params
+ const serviceClient = await createServiceClient()
+
+ // Check caller has permission
+ const { data: callerMembership } = await serviceClient
+ .from('company_members')
+ .select('role')
+ .eq('company_id', companyId)
+ .eq('user_id', user.id)
+ .single()
+
+ if (!callerMembership || !['owner', 'admin'].includes(callerMembership.role)) {
+ return NextResponse.json({ error: 'Behörighet saknas.' }, { status: 403 })
+ }
+
+ // Look up the invitation
+ const { data: invitation } = await serviceClient
+ .from('company_invitations')
+ .select('id, company_id, status')
+ .eq('id', inviteId)
+ .eq('company_id', companyId)
+ .single()
+
+ if (!invitation) {
+ return NextResponse.json({ error: 'Inbjudan hittades inte.' }, { status: 404 })
+ }
+
+ if (invitation.status !== 'pending') {
+ return NextResponse.json({ error: 'Inbjudan är inte väntande.' }, { status: 400 })
+ }
+
+ // Revoke the invitation
+ const { error } = await serviceClient
+ .from('company_invitations')
+ .update({ status: 'revoked' })
+ .eq('id', inviteId)
+ .eq('company_id', companyId)
+
+ if (error) {
+ return NextResponse.json({ error: 'Kunde inte återkalla inbjudan.' }, { status: 500 })
+ }
+
+ return NextResponse.json({ data: { revoked: inviteId } })
+}
diff --git a/app/api/company/members/invite/route.ts b/app/api/company/members/invite/route.ts
new file mode 100644
index 00000000..a851383f
--- /dev/null
+++ b/app/api/company/members/invite/route.ts
@@ -0,0 +1,158 @@
+import { createClient, createServiceClient } from '@/lib/supabase/server'
+import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
+import { generateInviteToken, getInviteExpiry } from '@/lib/auth/invite-tokens'
+import { getEmailService } from '@/lib/email/service'
+import {
+ generateInviteEmailSubject,
+ generateInviteEmailHtml,
+ generateInviteEmailText,
+} from '@/lib/email/invite-templates'
+
+/**
+ * POST /api/company/members/invite
+ * Invite a user to the current company (e.g., a client as viewer).
+ * Only company owners and admins can invite.
+ */
+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 companyId = await requireCompanyId(supabase, user.id)
+ const serviceClient = await createServiceClient()
+
+ // Check caller has permission
+ const { data: callerMembership } = await serviceClient
+ .from('company_members')
+ .select('role')
+ .eq('company_id', companyId)
+ .eq('user_id', user.id)
+ .single()
+
+ if (!callerMembership || !['owner', 'admin'].includes(callerMembership.role)) {
+ return NextResponse.json({ error: 'Behörighet saknas.' }, { status: 403 })
+ }
+
+ const body = await request.json()
+ const email = (body.email as string || '').trim().toLowerCase()
+ const role = (body.role as string) || 'viewer'
+
+ if (!email || !email.includes('@')) {
+ return NextResponse.json({ error: 'Ogiltig e-postadress.' }, { status: 400 })
+ }
+
+ if (!['admin', 'member', 'viewer'].includes(role)) {
+ return NextResponse.json({ error: 'Ogiltig roll.' }, { status: 400 })
+ }
+
+ // Check if email is already a member of this company
+ const { data: existingMembers } = await serviceClient
+ .from('company_members')
+ .select('id, user_id')
+ .eq('company_id', companyId)
+
+ if (existingMembers && existingMembers.length > 0) {
+ const memberUserIds = existingMembers.map((m) => m.user_id)
+ const { data: memberProfiles } = await serviceClient
+ .from('profiles')
+ .select('id, email')
+ .in('id', memberUserIds)
+
+ const alreadyMember = memberProfiles?.some(
+ (p) => p.email?.toLowerCase() === email
+ )
+ if (alreadyMember) {
+ return NextResponse.json({ error: 'Denna person är redan medlem.' }, { status: 409 })
+ }
+ }
+
+ // Check for existing pending invite
+ const { data: existingInvite } = await serviceClient
+ .from('company_invitations')
+ .select('id, status')
+ .eq('company_id', companyId)
+ .eq('email', email)
+ .single()
+
+ if (existingInvite && existingInvite.status === 'pending') {
+ return NextResponse.json({ error: 'En inbjudan har redan skickats till denna e-post.' }, { status: 409 })
+ }
+
+ // Get company name for the email
+ const { data: company } = await serviceClient
+ .from('companies')
+ .select('name')
+ .eq('id', companyId)
+ .single()
+
+ // Generate token
+ const { token, hash } = generateInviteToken()
+ const expiresAt = getInviteExpiry()
+
+ // Upsert invitation
+ if (existingInvite) {
+ const { error } = await serviceClient
+ .from('company_invitations')
+ .update({
+ token_hash: hash,
+ invited_by: user.id,
+ status: 'pending',
+ expires_at: expiresAt.toISOString(),
+ role,
+ })
+ .eq('id', existingInvite.id)
+
+ if (error) {
+ return NextResponse.json({ error: 'Kunde inte skapa inbjudan.' }, { status: 500 })
+ }
+ } else {
+ const { error } = await serviceClient
+ .from('company_invitations')
+ .insert({
+ company_id: companyId,
+ email,
+ role,
+ token_hash: hash,
+ invited_by: user.id,
+ status: 'pending',
+ expires_at: expiresAt.toISOString(),
+ })
+
+ if (error) {
+ return NextResponse.json({ error: 'Kunde inte skapa inbjudan.' }, { status: 500 })
+ }
+ }
+
+ // Send email
+ const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
+ const emailService = getEmailService()
+ if (emailService.isConfigured()) {
+ const inviteUrl = `${appUrl}/invite/${token}`
+
+ const emailData = {
+ companyName: company?.name || 'Företag',
+ inviterEmail: user.email || '',
+ inviteUrl,
+ }
+
+ const result = await emailService.sendEmail({
+ to: email,
+ subject: generateInviteEmailSubject(emailData),
+ html: generateInviteEmailHtml(emailData),
+ text: generateInviteEmailText(emailData),
+ })
+
+ if (!result.success) {
+ console.error('[company/members/invite] email send failed:', result.error)
+ }
+ }
+
+ // In development, return the invite URL directly (no email service)
+ const isDev = process.env.NODE_ENV === 'development'
+ const devInviteUrl = isDev ? `${appUrl}/invite/${token}` : undefined
+
+ return NextResponse.json({
+ data: { email, status: 'pending', ...(isDev && { inviteUrl: devInviteUrl }) },
+ })
+}
diff --git a/app/api/company/members/route.ts b/app/api/company/members/route.ts
new file mode 100644
index 00000000..946ee84f
--- /dev/null
+++ b/app/api/company/members/route.ts
@@ -0,0 +1,80 @@
+import { createClient, createServiceClient } from '@/lib/supabase/server'
+import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
+
+/**
+ * GET /api/company/members
+ * Returns members and pending invitations for the current company.
+ */
+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 companyId = await requireCompanyId(supabase, user.id)
+ const serviceClient = await createServiceClient()
+
+ // Fetch members (source column may not exist if migration not yet applied)
+ let members: { id: string; user_id: string; role: string; source?: string; joined_at: string }[] | null = null
+
+ const { data: membersWithSource, error: membersError } = await serviceClient
+ .from('company_members')
+ .select('id, user_id, role, source, joined_at')
+ .eq('company_id', companyId)
+ .order('joined_at', { ascending: true })
+
+ if (membersError) {
+ // Fallback: query without source column
+ const { data: membersFallback, error: fallbackError } = await serviceClient
+ .from('company_members')
+ .select('id, user_id, role, joined_at')
+ .eq('company_id', companyId)
+ .order('joined_at', { ascending: true })
+
+ if (fallbackError) {
+ return NextResponse.json({ error: 'Kunde inte hämta medlemmar.' }, { status: 500 })
+ }
+ members = (membersFallback || []).map((m) => ({ ...m, source: 'direct' as const }))
+ } else {
+ members = membersWithSource
+ }
+
+ // Fetch emails from profiles
+ const userIds = (members || []).map((m) => m.user_id)
+ const { data: profiles } = userIds.length > 0
+ ? await serviceClient
+ .from('profiles')
+ .select('id, email')
+ .in('id', userIds)
+ : { data: [] }
+
+ const emailMap = new Map((profiles || []).map((p) => [p.id, p.email]))
+
+ // Fetch pending company invitations
+ const { data: invitations } = await serviceClient
+ .from('company_invitations')
+ .select('id, email, role, status, expires_at, created_at')
+ .eq('company_id', companyId)
+ .eq('status', 'pending')
+ .order('created_at', { ascending: false })
+
+ // Get current user's role
+ const currentMember = members?.find((m) => m.user_id === user.id)
+ const canInvite = currentMember?.role === 'owner' || currentMember?.role === 'admin'
+
+ return NextResponse.json({
+ data: {
+ members: (members || []).map((m) => ({
+ id: m.id,
+ user_id: m.user_id,
+ email: emailMap.get(m.user_id) || '',
+ role: m.role,
+ source: m.source,
+ joined_at: m.joined_at,
+ is_current_user: m.user_id === user.id,
+ })),
+ invitations: invitations || [],
+ canInvite,
+ },
+ })
+}
diff --git a/app/api/customers/[id]/route.ts b/app/api/customers/[id]/route.ts
index 04511729..4a9a7974 100644
--- a/app/api/customers/[id]/route.ts
+++ b/app/api/customers/[id]/route.ts
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server'
import { validateBody } from '@/lib/api/validate'
import { UpdateCustomerSchema } from '@/lib/api/schemas'
import { validateVatNumber } from '@/lib/vat/vies-client'
+import { requireCompanyId } from '@/lib/company/context'
import { createLogger } from '@/lib/logger'
const log = createLogger('api/customers/[id]')
@@ -22,11 +23,13 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('customers')
.select('*')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (error) {
@@ -41,7 +44,7 @@ export async function GET(
.from('invoices')
.select('id, invoice_number, invoice_date, due_date, status, total, currency')
.eq('customer_id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.order('invoice_date', { ascending: false })
return NextResponse.json({
@@ -67,6 +70,8 @@ export async function PATCH(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const result = await validateBody(request, UpdateCustomerSchema)
if (!result.success) return result.response
const body = result.data
@@ -91,7 +96,7 @@ export async function PATCH(
.from('customers')
.update(updateData)
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.select()
.single()
@@ -113,7 +118,7 @@ export async function PATCH(
vat_number_validated_at: new Date().toISOString(),
})
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
data.vat_number_validated = true
data.vat_number_validated_at = new Date().toISOString()
@@ -125,7 +130,7 @@ export async function PATCH(
vat_number_validated_at: null,
})
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
data.vat_number_validated = false
data.vat_number_validated_at = null
@@ -139,7 +144,7 @@ export async function PATCH(
vat_number_validated_at: null,
})
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
data.vat_number_validated = false
data.vat_number_validated_at = null
@@ -167,11 +172,13 @@ export async function DELETE(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { error } = await supabase
.from('customers')
.delete()
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
diff --git a/app/api/customers/route.ts b/app/api/customers/route.ts
index 2eb12d97..ed8b6e80 100644
--- a/app/api/customers/route.ts
+++ b/app/api/customers/route.ts
@@ -5,6 +5,7 @@ import { ensureInitialized } from '@/lib/init'
import { validateBody } from '@/lib/api/validate'
import { CreateCustomerSchema } from '@/lib/api/schemas'
import { validateVatNumber } from '@/lib/vat/vies-client'
+import { requireCompanyId } from '@/lib/company/context'
import { createLogger } from '@/lib/logger'
import type { Customer } from '@/types'
@@ -21,10 +22,12 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('customers')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.order('name', { ascending: true })
if (error) {
@@ -43,6 +46,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const result = await validateBody(request, CreateCustomerSchema)
if (!result.success) return result.response
const body = result.data
@@ -51,6 +56,7 @@ export async function POST(request: Request) {
.from('customers')
.insert({
user_id: user.id,
+ company_id: companyId,
name: body.name,
customer_type: body.customer_type,
email: body.email,
@@ -84,7 +90,7 @@ export async function POST(request: Request) {
vat_number_validated_at: new Date().toISOString(),
})
.eq('id', data.id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
data.vat_number_validated = true
data.vat_number_validated_at = new Date().toISOString()
@@ -96,7 +102,7 @@ export async function POST(request: Request) {
await eventBus.emit({
type: 'customer.created',
- payload: { customer: data as Customer, userId: user.id },
+ payload: { customer: data as Customer, companyId, userId: user.id },
})
return NextResponse.json({ data })
diff --git a/app/api/deadlines/[id]/complete/route.ts b/app/api/deadlines/[id]/complete/route.ts
index 5da624d9..06a7f49f 100644
--- a/app/api/deadlines/[id]/complete/route.ts
+++ b/app/api/deadlines/[id]/complete/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
/**
* POST /api/deadlines/[id]/complete
@@ -20,12 +21,14 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// First, get current deadline state
const { data: existing, error: fetchError } = await supabase
.from('deadlines')
.select('is_completed')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchError) {
@@ -44,7 +47,7 @@ export async function POST(
completed_at: newCompletedState ? new Date().toISOString() : null,
})
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.select('*, customer:customers(id, name)')
.single()
diff --git a/app/api/deadlines/[id]/route.ts b/app/api/deadlines/[id]/route.ts
index 9d9c21ec..e21a1c99 100644
--- a/app/api/deadlines/[id]/route.ts
+++ b/app/api/deadlines/[id]/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
import type { CreateDeadlineInput } from '@/types'
/**
@@ -21,11 +22,13 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('deadlines')
.select('*, customer:customers(id, name)')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (error) {
@@ -57,6 +60,8 @@ export async function PUT(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const body: Partial = await request.json()
// First, get existing deadline to verify ownership
@@ -64,7 +69,7 @@ export async function PUT(
.from('deadlines')
.select('*')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchError) {
@@ -89,7 +94,7 @@ export async function PUT(
.from('deadlines')
.update(updateData)
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.select('*, customer:customers(id, name)')
.single()
@@ -119,11 +124,13 @@ export async function DELETE(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { error } = await supabase
.from('deadlines')
.delete()
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
diff --git a/app/api/deadlines/[id]/status/route.ts b/app/api/deadlines/[id]/status/route.ts
index a008ddfb..514a1321 100644
--- a/app/api/deadlines/[id]/status/route.ts
+++ b/app/api/deadlines/[id]/status/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { updateDeadlineStatus, isValidTransition } from '@/lib/deadlines/status-engine'
+import { requireCompanyId } from '@/lib/company/context'
import type { DeadlineStatus } from '@/types'
/**
@@ -19,6 +20,8 @@ export async function PATCH(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { id } = await params
const body = await request.json()
@@ -41,7 +44,7 @@ export async function PATCH(
return NextResponse.json({ error: 'Invalid status' }, { status: 400 })
}
- const result = await updateDeadlineStatus(supabase, id, user.id, newStatus)
+ const result = await updateDeadlineStatus(supabase, id, companyId, newStatus)
if (!result.success) {
return NextResponse.json({ error: result.error }, { status: 400 })
@@ -66,13 +69,15 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { id } = await params
const { data: deadline, error } = await supabase
.from('deadlines')
.select('status, is_completed, due_date')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (error || !deadline) {
diff --git a/app/api/deadlines/route.ts b/app/api/deadlines/route.ts
index d15512ad..4faae8a7 100644
--- a/app/api/deadlines/route.ts
+++ b/app/api/deadlines/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { validateBody } from '@/lib/api/validate'
import { CreateDeadlineSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
/**
* GET /api/deadlines
@@ -23,6 +24,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Parse query params
const { searchParams } = new URL(request.url)
const status = searchParams.get('status') || 'all'
@@ -34,7 +37,7 @@ export async function GET(request: Request) {
let query = supabase
.from('deadlines')
.select('*, customer:customers(id, name)')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
// Apply filters
if (status === 'pending') {
@@ -79,6 +82,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, CreateDeadlineSchema)
if (!validation.success) return validation.response
const body = validation.data
@@ -88,6 +93,7 @@ export async function POST(request: Request) {
.from('deadlines')
.insert({
user_id: user.id,
+ company_id: companyId,
title: body.title,
due_date: body.due_date,
due_time: body.due_time || null,
diff --git a/app/api/documents/[id]/link/route.ts b/app/api/documents/[id]/link/route.ts
index 3bd1ab59..802e688a 100644
--- a/app/api/documents/[id]/link/route.ts
+++ b/app/api/documents/[id]/link/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { ensureInitialized } from '@/lib/init'
import { linkToJournalEntry } from '@/lib/core/documents/document-service'
+import { requireCompanyId } from '@/lib/company/context'
ensureInitialized()
@@ -25,6 +26,8 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { id } = await params
try {
@@ -39,7 +42,7 @@ export async function POST(
const document = await linkToJournalEntry(
supabase,
- user.id,
+ companyId,
id,
body.journal_entry_id,
body.journal_entry_line_id
diff --git a/app/api/documents/[id]/route.ts b/app/api/documents/[id]/route.ts
index 54c9a1fe..aa0cdd74 100644
--- a/app/api/documents/[id]/route.ts
+++ b/app/api/documents/[id]/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { ensureInitialized } from '@/lib/init'
+import { requireCompanyId } from '@/lib/company/context'
ensureInitialized()
@@ -20,6 +21,8 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { id } = await params
// Fetch document record
@@ -27,7 +30,7 @@ export async function GET(
.from('document_attachments')
.select('*')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (docError || !doc) {
diff --git a/app/api/documents/[id]/verify/route.ts b/app/api/documents/[id]/verify/route.ts
index 33d499e2..e72a5932 100644
--- a/app/api/documents/[id]/verify/route.ts
+++ b/app/api/documents/[id]/verify/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { ensureInitialized } from '@/lib/init'
import { verifyIntegrity } from '@/lib/core/documents/document-service'
+import { requireCompanyId } from '@/lib/company/context'
ensureInitialized()
@@ -21,10 +22,12 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { id } = await params
try {
- const result = await verifyIntegrity(supabase, user.id, id)
+ const result = await verifyIntegrity(supabase, companyId, id)
return NextResponse.json({ data: result })
} catch (error) {
diff --git a/app/api/documents/[id]/versions/route.ts b/app/api/documents/[id]/versions/route.ts
index 5ec6b8e5..2459d087 100644
--- a/app/api/documents/[id]/versions/route.ts
+++ b/app/api/documents/[id]/versions/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { ensureInitialized } from '@/lib/init'
import { createNewVersion } from '@/lib/core/documents/document-service'
+import { requireCompanyId } from '@/lib/company/context'
ensureInitialized()
@@ -24,6 +25,8 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { id } = await params
try {
@@ -68,14 +71,16 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { id } = await params
- // First, check if the document belongs to the user
+ // First, check if the document belongs to the company
const { data: doc, error: docError } = await supabase
.from('document_attachments')
.select('id, original_id')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (docError || !doc) {
@@ -89,7 +94,7 @@ export async function GET(
const { data: versions, error: versionsError } = await supabase
.from('document_attachments')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.or(`id.eq.${rootId},original_id.eq.${rootId}`)
.order('version', { ascending: true })
diff --git a/app/api/documents/counts/route.ts b/app/api/documents/counts/route.ts
index 2cf7e718..3e040984 100644
--- a/app/api/documents/counts/route.ts
+++ b/app/api/documents/counts/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
/**
* GET /api/documents/counts?journal_entry_ids=id1,id2,...
@@ -15,6 +16,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const idsParam = searchParams.get('journal_entry_ids')
@@ -35,7 +38,7 @@ export async function GET(request: Request) {
const { data, error } = await supabase
.from('document_attachments')
.select('journal_entry_id')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('is_current_version', true)
.in('journal_entry_id', ids)
diff --git a/app/api/documents/match-sweep/route.ts b/app/api/documents/match-sweep/route.ts
index c1562317..59b884a8 100644
--- a/app/api/documents/match-sweep/route.ts
+++ b/app/api/documents/match-sweep/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { runDocumentMatchingSweep } from '@/lib/documents/batch-match'
+import { requireCompanyId } from '@/lib/company/context'
export async function POST(request: Request) {
const supabase = await createClient()
@@ -10,6 +11,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Optional: pass specific inbox item IDs to match
let inboxItemIds: string[] | undefined
try {
@@ -22,7 +25,7 @@ export async function POST(request: Request) {
}
try {
- const result = await runDocumentMatchingSweep(supabase, user.id, inboxItemIds)
+ const result = await runDocumentMatchingSweep(supabase, companyId, inboxItemIds)
return NextResponse.json({ data: result })
} catch (error) {
console.error('[match-sweep] Failed:', error)
diff --git a/app/api/documents/route.ts b/app/api/documents/route.ts
index 7ed9e751..d30ff262 100644
--- a/app/api/documents/route.ts
+++ b/app/api/documents/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { ensureInitialized } from '@/lib/init'
import { uploadDocument } from '@/lib/core/documents/document-service'
+import { requireCompanyId } from '@/lib/company/context'
ensureInitialized()
@@ -24,6 +25,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
try {
const formData = await request.formData()
const file = formData.get('file') as File | null
@@ -38,7 +41,7 @@ export async function POST(request: Request) {
const buffer = await file.arrayBuffer()
- const document = await uploadDocument(supabase, user.id, {
+ const document = await uploadDocument(supabase, user.id, companyId, {
name: file.name,
buffer,
type: file.type,
@@ -77,6 +80,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const journalEntryId = searchParams.get('journal_entry_id')
const currentOnly = searchParams.get('current_only') !== 'false'
@@ -86,7 +91,7 @@ export async function GET(request: Request) {
let query = supabase
.from('document_attachments')
.select('*', { count: 'exact' })
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.order('created_at', { ascending: false })
.range(offset, offset + limit - 1)
diff --git a/app/api/events/__tests__/route.test.ts b/app/api/events/__tests__/route.test.ts
index 2df60e50..a56ef766 100644
--- a/app/api/events/__tests__/route.test.ts
+++ b/app/api/events/__tests__/route.test.ts
@@ -11,6 +11,11 @@ vi.mock('@/lib/supabase/server', () => ({
createClient: () => Promise.resolve(mockSupabase),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
// Mock API key auth
const mockValidateApiKey = vi.fn()
const mockExtractBearerToken = vi.fn()
diff --git a/app/api/events/route.ts b/app/api/events/route.ts
index 9d1410d5..76b996ae 100644
--- a/app/api/events/route.ts
+++ b/app/api/events/route.ts
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server'
import { extractBearerToken, validateApiKey, createServiceClientNoCookies } from '@/lib/auth/api-keys'
import { validateQuery } from '@/lib/api/validate'
import { EventsQuerySchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
import type { SupabaseClient } from '@supabase/supabase-js'
/**
@@ -40,6 +41,8 @@ export async function GET(request: Request) {
userId = user.id
}
+ const companyId = await requireCompanyId(supabase, userId)
+
// Validate query params
const result = validateQuery(request, EventsQuerySchema)
if (!result.success) return result.response
@@ -49,7 +52,7 @@ export async function GET(request: Request) {
let query = supabase
.from('event_log')
.select('sequence, event_type, entity_id, data, created_at')
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.order('sequence', { ascending: true })
.limit(limit)
diff --git a/app/api/extensions/[sector]/[slug]/data/route.ts b/app/api/extensions/[sector]/[slug]/data/route.ts
index e0b0cb95..106ac12a 100644
--- a/app/api/extensions/[sector]/[slug]/data/route.ts
+++ b/app/api/extensions/[sector]/[slug]/data/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(
request: Request,
@@ -13,6 +14,8 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const extensionId = `${sector}/${slug}`
const { searchParams } = new URL(request.url)
@@ -21,7 +24,7 @@ export async function GET(
let query = supabase
.from('extension_data')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('extension_id', extensionId)
const prefix = searchParams.get('prefix')
@@ -53,6 +56,8 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const body = await request.json()
const { key, value } = body
@@ -67,6 +72,7 @@ export async function POST(
.upsert(
{
user_id: user.id,
+ company_id: companyId,
extension_id: extensionId,
key,
value,
@@ -95,6 +101,8 @@ export async function DELETE(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const key = searchParams.get('key')
@@ -107,7 +115,7 @@ export async function DELETE(
const { error } = await supabase
.from('extension_data')
.delete()
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('extension_id', extensionId)
.eq('key', key)
diff --git a/app/api/extensions/[sector]/[slug]/settings/route.ts b/app/api/extensions/[sector]/[slug]/settings/route.ts
index 5339166e..aa2df852 100644
--- a/app/api/extensions/[sector]/[slug]/settings/route.ts
+++ b/app/api/extensions/[sector]/[slug]/settings/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(
_request: Request,
@@ -13,12 +14,14 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const extensionId = `${sector}/${slug}`
const { data } = await supabase
.from('extension_data')
.select('value')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('extension_id', extensionId)
.eq('key', 'settings')
.single()
@@ -38,6 +41,8 @@ export async function PATCH(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const body = await request.json()
const extensionId = `${sector}/${slug}`
@@ -45,7 +50,7 @@ export async function PATCH(
const { data: existing } = await supabase
.from('extension_data')
.select('value')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('extension_id', extensionId)
.eq('key', 'settings')
.single()
@@ -57,6 +62,7 @@ export async function PATCH(
.upsert(
{
user_id: user.id,
+ company_id: companyId,
extension_id: extensionId,
key: 'settings',
value: mergedSettings,
diff --git a/app/api/extensions/enable-banking/callback/route.ts b/app/api/extensions/enable-banking/callback/route.ts
index 4f35435c..47dbace5 100644
--- a/app/api/extensions/enable-banking/callback/route.ts
+++ b/app/api/extensions/enable-banking/callback/route.ts
@@ -81,7 +81,7 @@ export async function GET(request: Request) {
// Look up pending connection by oauth_state (CSRF-safe)
const { data: pendingConnection, error: findError } = await supabase
.from('bank_connections')
- .select('id, user_id')
+ .select('id, user_id, company_id')
.eq('oauth_state', state)
.eq('status', 'pending')
.single()
@@ -98,6 +98,7 @@ export async function GET(request: Request) {
}
const userId = pendingConnection.user_id
+ const companyId = pendingConnection.company_id
console.log('[enable-banking] Exchanging code for session', {
connectionId: pendingConnection.id,
@@ -166,7 +167,7 @@ export async function GET(request: Request) {
const { data: userSettings } = await supabase
.from('company_settings')
.select('onboarding_complete')
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
const redirectTarget = userSettings?.onboarding_complete
diff --git a/app/api/extensions/enable-banking/sync/cron/route.ts b/app/api/extensions/enable-banking/sync/cron/route.ts
index 81df6ee7..898781dc 100644
--- a/app/api/extensions/enable-banking/sync/cron/route.ts
+++ b/app/api/extensions/enable-banking/sync/cron/route.ts
@@ -143,7 +143,7 @@ export async function GET(request: Request) {
const { data: sieOverlap } = await supabase
.from('sie_imports')
.select('id')
- .eq('user_id', connection.user_id)
+ .eq('company_id', connection.company_id)
.eq('status', 'completed')
.gte('fiscal_year_end', fromDate)
.limit(1)
@@ -156,6 +156,7 @@ export async function GET(request: Request) {
const syncResults = await Promise.all(
accounts.map(account => syncAccountTransactions(
supabase,
+ connection.company_id,
connection.user_id,
connection.id,
account,
@@ -284,7 +285,7 @@ async function sendConsentExpiryNotification(
const { data: companySettings } = await supabase
.from('company_settings')
.select('company_name')
- .eq('user_id', userId)
+ .eq('company_id', connection.company_id)
.single()
const emailData = {
diff --git a/app/api/extensions/ext/[...path]/__tests__/route.test.ts b/app/api/extensions/ext/[...path]/__tests__/route.test.ts
index 33c475d8..f0c0ae92 100644
--- a/app/api/extensions/ext/[...path]/__tests__/route.test.ts
+++ b/app/api/extensions/ext/[...path]/__tests__/route.test.ts
@@ -15,6 +15,11 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
vi.mock('@/lib/extensions/context-factory', () => ({
createExtensionContext: vi.fn().mockReturnValue({
userId: 'user-1',
diff --git a/app/api/extensions/ext/[...path]/route.ts b/app/api/extensions/ext/[...path]/route.ts
index d7affc1a..3be731a7 100644
--- a/app/api/extensions/ext/[...path]/route.ts
+++ b/app/api/extensions/ext/[...path]/route.ts
@@ -4,6 +4,7 @@ import { ensureInitialized } from '@/lib/init'
import { extensionRegistry } from '@/lib/extensions/registry'
import { createExtensionContext } from '@/lib/extensions/context-factory'
import { hasAiConsent, isAiExtension } from '@/lib/extensions/ai-consent'
+import { requireCompanyId } from '@/lib/company/context'
import type { ApiRouteDefinition } from '@/lib/extensions/types'
ensureInitialized()
@@ -117,9 +118,11 @@ async function handleRequest(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// AI consent check
if (isAiExtension(extensionId)) {
- const consented = await hasAiConsent(supabase, user.id, extensionId)
+ const consented = await hasAiConsent(supabase, companyId, extensionId)
if (!consented) {
return NextResponse.json(
{ error: 'AI consent required', code: 'AI_CONSENT_REQUIRED' },
@@ -145,7 +148,7 @@ async function handleRequest(
}
// Build context and dispatch
- const ctx = createExtensionContext(supabase, user.id, extensionId)
+ const ctx = createExtensionContext(supabase, user.id, companyId, extensionId)
return matchedRoute.handler(handlerRequest, ctx)
}
diff --git a/app/api/extensions/invoice-inbox/webhook/__tests__/route.test.ts b/app/api/extensions/invoice-inbox/webhook/__tests__/route.test.ts
index 3473c180..ed2688c4 100644
--- a/app/api/extensions/invoice-inbox/webhook/__tests__/route.test.ts
+++ b/app/api/extensions/invoice-inbox/webhook/__tests__/route.test.ts
@@ -142,7 +142,7 @@ describe('Invoice Inbox Webhook Route', () => {
created_at: '2024-06-15T10:00:00Z',
})
mockExtractAttachments.mockReturnValue([])
- mockResolveUserFromEmail.mockResolvedValue('user-1')
+ mockResolveUserFromEmail.mockResolvedValue({ userId: 'user-1', companyId: 'company-1' })
// Insert inbox item with error status
enqueueMany([
diff --git a/app/api/extensions/invoice-inbox/webhook/route.ts b/app/api/extensions/invoice-inbox/webhook/route.ts
index 4eaae8bf..d9b6e855 100644
--- a/app/api/extensions/invoice-inbox/webhook/route.ts
+++ b/app/api/extensions/invoice-inbox/webhook/route.ts
@@ -79,13 +79,15 @@ export async function POST(request: Request) {
const supabase = createServiceClient()
// Resolve user from recipient email
- const userId = await resolveUserFromEmail(payload.to, supabase)
+ const resolved = await resolveUserFromEmail(payload.to, supabase)
- if (!userId) {
+ if (!resolved) {
console.warn(`[document-inbox] No user found for email: ${payload.to}`)
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
+ const { userId, companyId } = resolved
+
// Build raw email payload for BFL 7:2 archiving (no binary attachment content)
const rawEmailPayload = buildRawEmailPayload(body, payload)
@@ -96,6 +98,7 @@ export async function POST(request: Request) {
await supabase
.from('invoice_inbox_items')
.insert({
+ company_id: companyId,
user_id: userId,
status: 'error',
source: 'email',
@@ -131,6 +134,7 @@ export async function POST(request: Request) {
const { data: document, error: docError } = await supabase
.from('document_attachments')
.insert({
+ company_id: companyId,
user_id: userId,
storage_path: storagePath,
file_name: attachment.filename,
@@ -159,6 +163,7 @@ export async function POST(request: Request) {
const { data: inboxItem, error: itemError } = await supabase
.from('invoice_inbox_items')
.insert({
+ company_id: companyId,
user_id: userId,
status: 'processing',
source: 'email',
@@ -197,7 +202,7 @@ export async function POST(request: Request) {
const { data: suppliers } = await supabase
.from('suppliers')
.select('*')
- .eq('user_id', userId)
+ .eq('company_id', companyId)
if (suppliers && suppliers.length > 0) {
const match = matchSupplier(extraction, suppliers)
@@ -222,7 +227,7 @@ export async function POST(request: Request) {
// Use pre-extracted receipt data from unified call
const { data: urlData } = supabase.storage.from('documents').getPublicUrl(storagePath)
- const result = await processReceiptFromDocument(supabase, userId, attachment.content, attachment.content_type, {
+ const result = await processReceiptFromDocument(supabase, userId, companyId, attachment.content, attachment.content_type, {
documentId: document.id,
source: 'email',
emailFrom: payload.from,
diff --git a/app/api/import/bank-file/execute/route.ts b/app/api/import/bank-file/execute/route.ts
index 61f3d1ab..23aec731 100644
--- a/app/api/import/bank-file/execute/route.ts
+++ b/app/api/import/bank-file/execute/route.ts
@@ -4,6 +4,7 @@ import { eventBus } from '@/lib/events'
import { ensureInitialized } from '@/lib/init'
import { ingestTransactions, type RawTransaction } from '@/lib/transactions/ingest'
import { generateExternalId } from '@/lib/import/bank-file/parser'
+import { requireCompanyId } from '@/lib/company/context'
import type { ParsedBankTransaction, BankFileFormatId } from '@/lib/import/bank-file/types'
import type { Transaction } from '@/types'
@@ -33,6 +34,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const body: ExecuteRequest = await request.json()
const { transactions, format, filename, file_hash, skip_duplicates: _skip_duplicates = true, auto_categorize: _auto_categorize = true } = body
@@ -46,6 +49,7 @@ export async function POST(request: Request) {
.from('bank_file_imports')
.upsert({
user_id: user.id,
+ company_id: companyId,
filename,
file_hash,
file_format: format,
@@ -76,7 +80,7 @@ export async function POST(request: Request) {
}))
// Run ingestion pipeline
- const ingestResult = await ingestTransactions(supabase, user.id, rawTransactions)
+ const ingestResult = await ingestTransactions(supabase, companyId, user.id, rawTransactions)
// Update import record with results
await supabase
@@ -106,6 +110,7 @@ export async function POST(request: Request) {
payload: {
transactions: importedTransactions as Transaction[],
userId: user.id,
+ companyId,
},
})
}
diff --git a/app/api/import/bank-file/parse/route.ts b/app/api/import/bank-file/parse/route.ts
index bd57eda9..22175ca5 100644
--- a/app/api/import/bank-file/parse/route.ts
+++ b/app/api/import/bank-file/parse/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { parseBankFile, generateFileHash, detectFileFormat } from '@/lib/import/bank-file/parser'
import { decodeFileContent } from '@/lib/import/bank-file/encoding'
+import { requireCompanyId } from '@/lib/company/context'
import type { BankFileFormatId } from '@/lib/import/bank-file/types'
/**
@@ -18,6 +19,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const formData = await request.formData()
const file = formData.get('file') as File | null
const formatOverride = formData.get('format') as BankFileFormatId | null
@@ -41,7 +44,7 @@ export async function POST(request: Request) {
const { data: existingImport } = await supabase
.from('bank_file_imports')
.select('id, status, imported_count, created_at')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('file_hash', fileHash)
.single()
@@ -67,7 +70,7 @@ export async function POST(request: Request) {
const { count } = await supabase
.from('transactions')
.select('*', { count: 'exact', head: true })
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.gte('date', parseResult.date_from || '1970-01-01')
.lte('date', parseResult.date_to || '2099-12-31')
diff --git a/app/api/import/sie/[id]/route.ts b/app/api/import/sie/[id]/route.ts
index 23c5ed8f..8b30cc74 100644
--- a/app/api/import/sie/[id]/route.ts
+++ b/app/api/import/sie/[id]/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
/**
* GET /api/import/sie/[id]
@@ -20,11 +21,13 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('sie_imports')
.select('*')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (error) {
@@ -62,12 +65,14 @@ export async function DELETE(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Check current status before deleting
const { data: importRecord } = await supabase
.from('sie_imports')
.select('status')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (!importRecord) {
@@ -84,7 +89,7 @@ export async function DELETE(
.from('sie_imports')
.delete()
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
diff --git a/app/api/import/sie/create-accounts/route.ts b/app/api/import/sie/create-accounts/route.ts
index c2e0f866..755b9b8d 100644
--- a/app/api/import/sie/create-accounts/route.ts
+++ b/app/api/import/sie/create-accounts/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
import type { SIEAccount } from '@/lib/import/types'
/**
@@ -63,6 +64,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
try {
const body = await request.json()
const accounts: SIEAccount[] = body.accounts
@@ -80,6 +83,7 @@ export async function POST(request: Request) {
return {
user_id: user.id,
+ company_id: companyId,
account_number: account.number,
account_name: account.name,
account_class: accountClass,
diff --git a/app/api/import/sie/execute/route.ts b/app/api/import/sie/execute/route.ts
index 57432285..9d9b7f74 100644
--- a/app/api/import/sie/execute/route.ts
+++ b/app/api/import/sie/execute/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { fetchAllRows } from '@/lib/supabase/fetch-all'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
import { parseSIEFile, detectEncoding, decodeBuffer } from '@/lib/import/sie-parser'
import { suggestMappings } from '@/lib/import/account-mapper'
import { executeSIEImport } from '@/lib/import/sie-import'
@@ -26,6 +27,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
try {
// Get form data with file and options
const formData = await request.formData()
@@ -63,7 +66,7 @@ export async function POST(request: Request) {
const { data: storedMappings } = await supabase
.from('sie_account_mappings')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
mappings = suggestMappings(
parsed.accounts,
@@ -90,14 +93,15 @@ export async function POST(request: Request) {
...new Set(mappings.filter((m) => m.targetAccount).map((m) => m.targetAccount)),
]
- const existingAccounts = await fetchAllRows(({ from, to }) =>
+ const allCompanyAccounts = await fetchAllRows(({ from, to }) =>
supabase
.from('chart_of_accounts')
.select('account_number')
- .eq('user_id', user.id)
- .in('account_number', mappedAccountNumbers)
+ .eq('company_id', companyId)
.range(from, to)
)
+ const mappedSet = new Set(mappedAccountNumbers)
+ const existingAccounts = allCompanyAccounts.filter((a) => mappedSet.has(a.account_number))
// Build a lookup from SIE mappings for account names (used for bas_range accounts)
const mappingNameLookup = new Map()
@@ -116,6 +120,7 @@ export async function POST(request: Request) {
// Account exists in BAS reference — use full metadata
return {
user_id: user.id,
+ company_id: companyId,
account_number: ref.account_number,
account_name: ref.account_name,
account_class: ref.account_class,
@@ -146,6 +151,7 @@ export async function POST(request: Request) {
return {
user_id: user.id,
+ company_id: companyId,
account_number: num,
account_name: accountName,
account_class: accountClass,
@@ -176,6 +182,7 @@ export async function POST(request: Request) {
// Execute the import
const result = await executeSIEImport(
supabase,
+ companyId,
user.id,
parsed,
mappings,
diff --git a/app/api/import/sie/mappings/route.ts b/app/api/import/sie/mappings/route.ts
index 6ddd9688..5453c91c 100644
--- a/app/api/import/sie/mappings/route.ts
+++ b/app/api/import/sie/mappings/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { saveMappings } from '@/lib/import/sie-import'
+import { requireCompanyId } from '@/lib/company/context'
import type { AccountMapping } from '@/lib/import/types'
/**
@@ -18,10 +19,12 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('sie_account_mappings')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.order('source_account')
if (error) {
@@ -46,6 +49,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const body = await request.json()
const mappings: AccountMapping[] = body.mappings
@@ -79,6 +84,8 @@ export async function PUT(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const body = await request.json()
const { sourceAccount, targetAccount } = body
@@ -93,6 +100,7 @@ export async function PUT(request: Request) {
.from('sie_account_mappings')
.upsert({
user_id: user.id,
+ company_id: companyId,
source_account: sourceAccount,
target_account: targetAccount,
confidence: 1.0,
@@ -125,6 +133,8 @@ export async function DELETE(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const sourceAccount = searchParams.get('sourceAccount')
@@ -133,7 +143,7 @@ export async function DELETE(request: Request) {
const { error } = await supabase
.from('sie_account_mappings')
.delete()
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('source_account', sourceAccount)
if (error) {
@@ -144,7 +154,7 @@ export async function DELETE(request: Request) {
const { error } = await supabase
.from('sie_account_mappings')
.delete()
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
diff --git a/app/api/import/sie/parse/route.ts b/app/api/import/sie/parse/route.ts
index 684377ff..0385f109 100644
--- a/app/api/import/sie/parse/route.ts
+++ b/app/api/import/sie/parse/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
import {
parseSIEFile,
validateSIEFile,
@@ -27,6 +28,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
try {
// Get form data with file
const formData = await request.formData()
@@ -91,7 +94,7 @@ export async function POST(request: Request) {
const { data: storedMappings } = await supabase
.from('sie_account_mappings')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
// Match against the full BAS reference (1,276 accounts) instead of only
// the user's active chart (~40 accounts). Accounts that match will be
diff --git a/app/api/import/sie/route.ts b/app/api/import/sie/route.ts
index e322606a..52cc4beb 100644
--- a/app/api/import/sie/route.ts
+++ b/app/api/import/sie/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
/**
* GET /api/import/sie
@@ -16,6 +17,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Parse query params
const { searchParams } = new URL(request.url)
const limit = parseInt(searchParams.get('limit') || '20', 10)
@@ -25,7 +28,7 @@ export async function GET(request: Request) {
let query = supabase
.from('sie_imports')
.select('*', { count: 'exact' })
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.order('created_at', { ascending: false })
.range(offset, offset + limit - 1)
diff --git a/app/api/invoices/[id]/convert/route.ts b/app/api/invoices/[id]/convert/route.ts
index f1f6dac9..d5b4da30 100644
--- a/app/api/invoices/[id]/convert/route.ts
+++ b/app/api/invoices/[id]/convert/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { eventBus } from '@/lib/events'
import { ensureInitialized } from '@/lib/init'
+import { requireCompanyId } from '@/lib/company/context'
import type { Invoice } from '@/types'
ensureInitialized()
@@ -25,12 +26,14 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch proforma with items
const { data: proforma, error: proformaError } = await supabase
.from('invoices')
.select('*, items:invoice_items(*)')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (proformaError || !proforma) {
@@ -61,6 +64,7 @@ export async function POST(
.from('invoices')
.insert({
user_id: user.id,
+ company_id: companyId,
customer_id: proforma.customer_id,
invoice_number: invoiceNumber,
invoice_date: new Date().toISOString().split('T')[0],
@@ -129,7 +133,7 @@ export async function POST(
if (completeInvoice) {
await eventBus.emit({
type: 'invoice.created',
- payload: { invoice: completeInvoice as Invoice, userId: user.id },
+ payload: { invoice: completeInvoice as Invoice, companyId, userId: user.id },
})
}
diff --git a/app/api/invoices/[id]/mark-paid/__tests__/route.test.ts b/app/api/invoices/[id]/mark-paid/__tests__/route.test.ts
index baba81d1..ed802f3e 100644
--- a/app/api/invoices/[id]/mark-paid/__tests__/route.test.ts
+++ b/app/api/invoices/[id]/mark-paid/__tests__/route.test.ts
@@ -17,6 +17,10 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
const mockCreateInvoicePaymentJournalEntry = vi.fn()
const mockCreateInvoiceCashEntry = vi.fn()
vi.mock('@/lib/bookkeeping/invoice-entries', () => ({
@@ -137,6 +141,7 @@ describe('POST /api/invoices/[id]/mark-paid', () => {
expect(body.journal_entry_id).toBe('je-1')
expect(mockCreateInvoicePaymentJournalEntry).toHaveBeenCalledWith(
expect.anything(),
+ 'company-1',
'user-1',
expect.objectContaining({ id: 'inv-1' }),
expect.any(String),
@@ -172,6 +177,7 @@ describe('POST /api/invoices/[id]/mark-paid', () => {
expect(body.journal_entry_id).toBe('je-2')
expect(mockCreateInvoiceCashEntry).toHaveBeenCalledWith(
expect.anything(),
+ 'company-1',
'user-1',
expect.objectContaining({ id: 'inv-1' }),
expect.any(String),
@@ -235,6 +241,7 @@ describe('POST /api/invoices/[id]/mark-paid', () => {
// Should call createJournalEntry directly with custom lines
expect(mockCreateJournalEntry).toHaveBeenCalledWith(
expect.anything(),
+ 'company-1',
'user-1',
expect.objectContaining({
entry_date: '2025-03-17',
diff --git a/app/api/invoices/[id]/mark-paid/route.ts b/app/api/invoices/[id]/mark-paid/route.ts
index a2f57ab0..7c6d247e 100644
--- a/app/api/invoices/[id]/mark-paid/route.ts
+++ b/app/api/invoices/[id]/mark-paid/route.ts
@@ -7,6 +7,7 @@ import {
import { createJournalEntry, findFiscalPeriod } from '@/lib/bookkeeping/engine'
import { MarkInvoicePaidSchema } from '@/lib/api/schemas'
import { ensureInitialized } from '@/lib/init'
+import { requireCompanyId } from '@/lib/company/context'
import type { CreateJournalEntryInput, EntityType, Invoice } from '@/types'
ensureInitialized()
@@ -35,12 +36,14 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch invoice
const { data: invoice, error: invoiceError } = await supabase
.from('invoices')
.select('*, customer:customers(*), items:invoice_items(*)')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (invoiceError || !invoice) {
@@ -83,7 +86,7 @@ export async function POST(
const { data: settings } = await supabase
.from('company_settings')
.select('accounting_method, entity_type')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const accountingMethod = settings?.accounting_method || 'accrual'
@@ -107,7 +110,7 @@ export async function POST(
}
// User-provided lines from PaymentBookingDialog
- const fiscalPeriodId = await findFiscalPeriod(supabase, user.id, paymentDate)
+ const fiscalPeriodId = await findFiscalPeriod(supabase, companyId, paymentDate)
if (!fiscalPeriodId) {
return NextResponse.json(
{ error: 'Ingen öppen räkenskapsperiod för betalningsdatumet' },
@@ -125,12 +128,13 @@ export async function POST(
source_id: invoice.id,
lines: customLines,
}
- const journalEntry = await createJournalEntry(supabase, user.id, input)
+ const journalEntry = await createJournalEntry(supabase, companyId, user.id, input)
journalEntryId = journalEntry?.id ?? null
} else if (accountingMethod === 'accrual') {
// Faktureringsmetoden: clear receivable (Debit 1930, Credit 1510)
const journalEntry = await createInvoicePaymentJournalEntry(
supabase,
+ companyId,
user.id,
invoice as Invoice,
paymentDate,
@@ -142,6 +146,7 @@ export async function POST(
// Kontantmetoden: combined revenue entry (Debit 1930, Credit 30xx, Credit 26xx)
const journalEntry = await createInvoiceCashEntry(
supabase,
+ companyId,
user.id,
invoice as Invoice,
paymentDate,
@@ -168,7 +173,7 @@ export async function POST(
paid_amount: invoice.total,
})
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (updateError) {
return NextResponse.json({ error: 'Kunde inte uppdatera status' }, { status: 500 })
diff --git a/app/api/invoices/[id]/mark-sent/route.ts b/app/api/invoices/[id]/mark-sent/route.ts
index 6a5f4978..189dcfae 100644
--- a/app/api/invoices/[id]/mark-sent/route.ts
+++ b/app/api/invoices/[id]/mark-sent/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { createInvoiceJournalEntry } from '@/lib/bookkeeping/invoice-entries'
import { ensureInitialized } from '@/lib/init'
+import { requireCompanyId } from '@/lib/company/context'
import type { EntityType, Invoice } from '@/types'
ensureInitialized()
@@ -26,12 +27,14 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch invoice
const { data: invoice, error: invoiceError } = await supabase
.from('invoices')
.select('*, customer:customers(*), items:invoice_items(*)')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (invoiceError || !invoice) {
@@ -50,7 +53,7 @@ export async function POST(
.from('invoices')
.update({ status: 'sent' })
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (updateError) {
return NextResponse.json({ error: 'Kunde inte uppdatera status' }, { status: 500 })
@@ -60,7 +63,7 @@ export async function POST(
const { data: settings } = await supabase
.from('company_settings')
.select('accounting_method, entity_type')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const accountingMethod = settings?.accounting_method || 'accrual'
@@ -72,6 +75,7 @@ export async function POST(
try {
const journalEntry = await createInvoiceJournalEntry(
supabase,
+ companyId,
user.id,
invoice as Invoice,
(settings?.entity_type as EntityType) || 'enskild_firma',
diff --git a/app/api/invoices/[id]/pdf/route.ts b/app/api/invoices/[id]/pdf/route.ts
index 29757dba..8f1304d0 100644
--- a/app/api/invoices/[id]/pdf/route.ts
+++ b/app/api/invoices/[id]/pdf/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { renderToBuffer } from '@react-pdf/renderer'
import { InvoicePDF } from '@/lib/invoices/pdf-template'
+import { requireCompanyId } from '@/lib/company/context'
import type { Invoice, InvoiceItem, Customer, CompanySettings } from '@/types'
export async function GET(
@@ -17,6 +18,8 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch invoice with customer and items
const { data: invoice, error: invoiceError } = await supabase
.from('invoices')
@@ -26,7 +29,7 @@ export async function GET(
items:invoice_items(*)
`)
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (invoiceError || !invoice) {
@@ -37,7 +40,7 @@ export async function GET(
const { data: company, error: companyError } = await supabase
.from('company_settings')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (companyError || !company) {
diff --git a/app/api/invoices/[id]/route.ts b/app/api/invoices/[id]/route.ts
index 31837a4d..2dc601ef 100644
--- a/app/api/invoices/[id]/route.ts
+++ b/app/api/invoices/[id]/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
/**
* DELETE /api/invoices/[id]
@@ -21,12 +22,14 @@ export async function DELETE(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch invoice to verify ownership and status
const { data: invoice, error: fetchError } = await supabase
.from('invoices')
.select('id, status, user_id')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchError || !invoice) {
@@ -54,7 +57,7 @@ export async function DELETE(
.from('invoices')
.delete()
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (deleteError) {
return NextResponse.json({ error: deleteError.message }, { status: 500 })
diff --git a/app/api/invoices/[id]/send/__tests__/route.test.ts b/app/api/invoices/[id]/send/__tests__/route.test.ts
index 8631ceff..7bcc413f 100644
--- a/app/api/invoices/[id]/send/__tests__/route.test.ts
+++ b/app/api/invoices/[id]/send/__tests__/route.test.ts
@@ -19,6 +19,11 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
const mockRenderToBuffer = vi.fn()
vi.mock('@react-pdf/renderer', () => ({
renderToBuffer: (...args: unknown[]) => mockRenderToBuffer(...args),
@@ -184,6 +189,7 @@ describe('POST /api/invoices/[id]/send', () => {
)
expect(mockCreateInvoiceJournalEntry).toHaveBeenCalledWith(
expect.anything(),
+ 'company-1',
'user-1',
expect.objectContaining({ id: 'inv-1' }),
'enskild_firma'
diff --git a/app/api/invoices/[id]/send/route.ts b/app/api/invoices/[id]/send/route.ts
index b22fab23..6ae0cdd5 100644
--- a/app/api/invoices/[id]/send/route.ts
+++ b/app/api/invoices/[id]/send/route.ts
@@ -12,6 +12,7 @@ import {
} from '@/lib/email/invoice-templates'
import { createInvoiceJournalEntry } from '@/lib/bookkeeping/invoice-entries'
import { uploadDocument } from '@/lib/core/documents/document-service'
+import { requireCompanyId } from '@/lib/company/context'
import type { Invoice, InvoiceItem, Customer, CompanySettings } from '@/types'
ensureInitialized()
@@ -29,6 +30,8 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Check if email is configured
const emailService = getEmailService()
if (!emailService.isConfigured()) {
@@ -47,7 +50,7 @@ export async function POST(
items:invoice_items(*)
`)
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (invoiceError || !invoice) {
@@ -67,7 +70,7 @@ export async function POST(
const { data: company, error: companyError } = await supabase
.from('company_settings')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (companyError || !company) {
@@ -161,7 +164,7 @@ export async function POST(
.from('invoices')
.update({ status: 'sent' })
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (updateError) {
console.error('Failed to update invoice status:', updateError)
@@ -175,6 +178,7 @@ export async function POST(
try {
const journalEntry = await createInvoiceJournalEntry(
supabase,
+ companyId,
user.id,
invoice as Invoice,
(company as CompanySettings).entity_type
@@ -196,7 +200,7 @@ export async function POST(
if (isRealInvoice) {
try {
const pdfArrayBuffer = new Uint8Array(pdfBuffer).buffer as ArrayBuffer
- await uploadDocument(supabase, user.id, {
+ await uploadDocument(supabase, user.id, companyId, {
name: filename,
buffer: pdfArrayBuffer,
type: 'application/pdf',
@@ -212,7 +216,7 @@ export async function POST(
await eventBus.emit({
type: 'invoice.sent',
- payload: { invoice: invoice as Invoice, userId: user.id },
+ payload: { invoice: invoice as Invoice, companyId, userId: user.id },
})
return NextResponse.json({
diff --git a/app/api/invoices/__tests__/route.test.ts b/app/api/invoices/__tests__/route.test.ts
index d7e68c95..2da2932f 100644
--- a/app/api/invoices/__tests__/route.test.ts
+++ b/app/api/invoices/__tests__/route.test.ts
@@ -17,6 +17,11 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
const mockGetVatRules = vi.fn()
const mockCalculateVat = vi.fn()
const mockGetAvailableVatRates = vi.fn()
diff --git a/app/api/invoices/preview-pdf/route.ts b/app/api/invoices/preview-pdf/route.ts
index 8f7d77df..cb9f64d0 100644
--- a/app/api/invoices/preview-pdf/route.ts
+++ b/app/api/invoices/preview-pdf/route.ts
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server'
import { renderToBuffer } from '@react-pdf/renderer'
import { InvoicePDF } from '@/lib/invoices/pdf-template'
import { getVatRules } from '@/lib/invoices/vat-rules'
+import { requireCompanyId } from '@/lib/company/context'
import type { Invoice, InvoiceItem, Customer, CompanySettings, InvoiceDocumentType } from '@/types'
/**
@@ -20,6 +21,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const body = await request.json()
const { customer_id, invoice_date, due_date, currency, items, your_reference, our_reference, notes, document_type } = body
@@ -32,7 +35,7 @@ export async function POST(request: Request) {
.from('customers')
.select('*')
.eq('id', customer_id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (customerError || !customer) {
@@ -43,7 +46,7 @@ export async function POST(request: Request) {
const { data: company, error: companyError } = await supabase
.from('company_settings')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (companyError || !company) {
diff --git a/app/api/invoices/route.ts b/app/api/invoices/route.ts
index 4724621f..5a24ed15 100644
--- a/app/api/invoices/route.ts
+++ b/app/api/invoices/route.ts
@@ -9,6 +9,7 @@ import { fetchExchangeRate, convertToSEK } from '@/lib/currency/riksbanken'
import {
createCreditNoteJournalEntry,
} from '@/lib/bookkeeping/invoice-entries'
+import { requireCompanyId } from '@/lib/company/context'
ensureInitialized()
@@ -21,6 +22,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const status = searchParams.get('status')
const limit = parseInt(searchParams.get('limit') || '50')
@@ -29,7 +32,7 @@ export async function GET(request: Request) {
let query = supabase
.from('invoices')
.select('*, customer:customers(*)', { count: 'exact' })
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.order('invoice_date', { ascending: false })
.range(offset, offset + limit - 1)
@@ -55,6 +58,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
let rawBody: unknown
try {
rawBody = await request.json()
@@ -78,7 +83,7 @@ export async function POST(request: Request) {
{ status: 400 },
)
}
- return createCreditNote(supabase, user.id, parsed.data)
+ return createCreditNote(supabase, companyId, user.id, parsed.data)
}
const parsed = CreateInvoiceSchema.safeParse(rawBody)
@@ -100,7 +105,7 @@ export async function POST(request: Request) {
.from('customers')
.select('*')
.eq('id', invoiceInput.customer_id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (customerError || !customer) {
@@ -178,6 +183,7 @@ export async function POST(request: Request) {
.from('invoices')
.insert({
user_id: user.id,
+ company_id: companyId,
customer_id: invoiceInput.customer_id,
invoice_number: invoiceNumber,
invoice_date: invoiceInput.invoice_date,
@@ -246,7 +252,7 @@ export async function POST(request: Request) {
if (completeInvoice && documentType === 'invoice') {
await eventBus.emit({
type: 'invoice.created',
- payload: { invoice: completeInvoice as Invoice, userId: user.id },
+ payload: { invoice: completeInvoice as Invoice, companyId, userId: user.id },
})
}
@@ -256,6 +262,7 @@ export async function POST(request: Request) {
// Create a credit note for an existing invoice
async function createCreditNote(
supabase: Awaited>,
+ companyId: string,
userId: string,
input: { credited_invoice_id: string; reason?: string }
) {
@@ -264,7 +271,7 @@ async function createCreditNote(
.from('invoices')
.select('*, items:invoice_items(*)')
.eq('id', input.credited_invoice_id)
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
if (originalError || !originalInvoice) {
@@ -300,6 +307,7 @@ async function createCreditNote(
.from('invoices')
.insert({
user_id: userId,
+ company_id: companyId,
customer_id: originalInvoice.customer_id,
invoice_number: creditNoteNumber,
invoice_date: new Date().toISOString().split('T')[0],
@@ -373,7 +381,7 @@ async function createCreditNote(
const { data: creditNoteSettings } = await supabase
.from('company_settings')
.select('entity_type, accounting_method')
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
const entityType = (creditNoteSettings?.entity_type as EntityType) || 'enskild_firma'
@@ -385,6 +393,7 @@ async function createCreditNote(
try {
const journalEntry = await createCreditNoteJournalEntry(
supabase,
+ companyId,
userId,
completeCreditNote as Invoice,
entityType,
@@ -402,7 +411,7 @@ async function createCreditNote(
await eventBus.emit({
type: 'credit_note.created',
- payload: { creditNote: completeCreditNote as CreditNote, userId },
+ payload: { creditNote: completeCreditNote as CreditNote, companyId, userId },
})
}
diff --git a/app/api/kpi/preferences/route.ts b/app/api/kpi/preferences/route.ts
index 624f8596..b3932cf1 100644
--- a/app/api/kpi/preferences/route.ts
+++ b/app/api/kpi/preferences/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { mergeWithDefaults } from '@/lib/reports/kpi-definitions'
+import { requireCompanyId } from '@/lib/company/context'
import type { KPIPreferences } from '@/types'
const EXTENSION_ID = 'core/kpi'
@@ -11,10 +12,12 @@ export async function GET() {
const { data: { user } } = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data } = await supabase
.from('extension_data')
.select('value')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('extension_id', EXTENSION_ID)
.eq('key', KEY)
.single()
@@ -28,6 +31,8 @@ export async function PUT(request: Request) {
const { data: { user } } = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ const companyId = await requireCompanyId(supabase, user.id)
+
let body: unknown
try {
body = await request.json()
@@ -64,6 +69,7 @@ export async function PUT(request: Request) {
.upsert(
{
user_id: user.id,
+ company_id: companyId,
extension_id: EXTENSION_ID,
key: KEY,
value: merged,
diff --git a/app/api/mcp-oauth/authorize/route.ts b/app/api/mcp-oauth/authorize/route.ts
index 2474bf60..40d60207 100644
--- a/app/api/mcp-oauth/authorize/route.ts
+++ b/app/api/mcp-oauth/authorize/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { createAuthCode } from '@/lib/auth/oauth-codes'
+import { requireCompanyId } from '@/lib/company/context'
/**
* OAuth 2.0 Authorization Endpoint.
@@ -88,11 +89,13 @@ export async function GET(request: Request) {
return buildLoginRedirect(request)
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Get company name for the consent page
const { data: settings } = await supabase
.from('company_settings')
.select('company_name')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const companyName = settings?.company_name || user.email
@@ -180,6 +183,8 @@ export async function POST(request: Request) {
return buildLoginRedirect(request)
}
+ await requireCompanyId(supabase, user.id)
+
// Parse form body
const formData = await request.formData()
const consent = formData.get('consent')
diff --git a/app/api/mcp-oauth/token/route.ts b/app/api/mcp-oauth/token/route.ts
index 2498f75f..abf35387 100644
--- a/app/api/mcp-oauth/token/route.ts
+++ b/app/api/mcp-oauth/token/route.ts
@@ -1,6 +1,7 @@
import { NextResponse } from 'next/server'
import { decryptAuthCode, verifyPkce, hashAuthCode } from '@/lib/auth/oauth-codes'
import { generateApiKey, createServiceClientNoCookies, ALL_SCOPES } from '@/lib/auth/api-keys'
+import { requireCompanyId } from '@/lib/company/context'
/**
* OAuth 2.0 Token Endpoint.
@@ -100,6 +101,9 @@ export async function POST(request: Request) {
.lt('created_at', new Date(Date.now() - 10 * 60 * 1000).toISOString())
.then(() => {})
+ // Resolve company context for the user
+ const companyId = await requireCompanyId(supabase, payload.userId)
+
// Create the API key now (after PKCE verification — prevents orphaned keys)
const { key, hash, prefix } = generateApiKey()
@@ -107,6 +111,7 @@ export async function POST(request: Request) {
.from('api_keys')
.insert({
user_id: payload.userId,
+ company_id: companyId,
key_hash: hash,
key_prefix: prefix,
name: 'MCP-klient (OAuth)',
diff --git a/app/api/pending-operations/[id]/commit/__tests__/route.test.ts b/app/api/pending-operations/[id]/commit/__tests__/route.test.ts
index 6f7da7e0..ca3427fc 100644
--- a/app/api/pending-operations/[id]/commit/__tests__/route.test.ts
+++ b/app/api/pending-operations/[id]/commit/__tests__/route.test.ts
@@ -15,6 +15,11 @@ vi.mock('@/lib/supabase/server', () => ({
}))
vi.mock('@/lib/init', () => ({ ensureInitialized: vi.fn() }))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
// Mock the counterparty templates (non-critical side effect)
vi.mock('@/lib/bookkeeping/counterparty-templates', () => ({
upsertCounterpartyTemplate: vi.fn().mockResolvedValue(undefined),
diff --git a/app/api/pending-operations/[id]/commit/route.ts b/app/api/pending-operations/[id]/commit/route.ts
index 38f4ab56..32fbeee8 100644
--- a/app/api/pending-operations/[id]/commit/route.ts
+++ b/app/api/pending-operations/[id]/commit/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { eventBus } from '@/lib/events'
import { ensureInitialized } from '@/lib/init'
+import { requireCompanyId } from '@/lib/company/context'
import { buildMappingResultFromCategory } from '@/lib/bookkeeping/category-mapping'
import { createTransactionJournalEntry } from '@/lib/bookkeeping/transaction-entries'
import { upsertCounterpartyTemplate } from '@/lib/bookkeeping/counterparty-templates'
@@ -48,13 +49,14 @@ ensureInitialized()
async function ensureFiscalPeriod(
supabase: Awaited>,
userId: string,
+ companyId: string,
date: string,
fiscalYearStartMonth: number = 1
): Promise {
const { data: existing } = await supabase
.from('fiscal_periods')
.select('id')
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.lte('period_start', date)
.gte('period_end', date)
.eq('is_closed', false)
@@ -91,6 +93,7 @@ async function ensureFiscalPeriod(
.from('fiscal_periods')
.upsert({
user_id: userId,
+ company_id: companyId,
name: periodName,
period_start: periodStart,
period_end: periodEnd,
@@ -108,6 +111,7 @@ async function ensureFiscalPeriod(
async function commitCategorizeTransaction(
supabase: Awaited>,
userId: string,
+ companyId: string,
params: Record
): Promise<{ data?: Record; error?: string; status?: number }> {
const txId = params.transaction_id as string
@@ -119,7 +123,7 @@ async function commitCategorizeTransaction(
.from('transactions')
.select('*')
.eq('id', txId)
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
if (fetchError || !transaction) {
@@ -136,7 +140,7 @@ async function commitCategorizeTransaction(
const { data: settings } = await supabase
.from('company_settings')
.select('entity_type, fiscal_year_start_month')
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
const entityType: EntityType = (settings?.entity_type as EntityType) || 'enskild_firma'
@@ -156,13 +160,13 @@ async function commitCategorizeTransaction(
}
// Ensure fiscal period exists
- await ensureFiscalPeriod(supabase, userId, transaction.date, fiscalYearStartMonth)
+ await ensureFiscalPeriod(supabase, userId, companyId, transaction.date, fiscalYearStartMonth)
// Create journal entry
let journalEntryId: string | null = null
try {
const journalEntry = await createTransactionJournalEntry(
- supabase, userId, transaction as Transaction, mappingResult
+ supabase, companyId, userId, transaction as Transaction, mappingResult
)
if (journalEntry) {
journalEntryId = journalEntry.id
@@ -202,6 +206,7 @@ async function commitCategorizeTransaction(
account: mappingResult.debit_account,
taxCode: mappingResult.vat_lines[0]?.account_number || '',
userId,
+ companyId,
},
})
@@ -211,12 +216,14 @@ async function commitCategorizeTransaction(
async function commitCreateCustomer(
supabase: Awaited>,
userId: string,
+ companyId: string,
params: Record
): Promise<{ data?: Record; error?: string; status?: number }> {
const { data, error } = await supabase
.from('customers')
.insert({
user_id: userId,
+ company_id: companyId,
name: params.name as string,
customer_type: params.customer_type as string,
email: (params.email as string) || null,
@@ -247,7 +254,7 @@ async function commitCreateCustomer(
vat_number_validated_at: new Date().toISOString(),
})
.eq('id', data.id)
- .eq('user_id', userId)
+ .eq('company_id', companyId)
}
} catch (err) {
log.warn('Auto-VIES validation failed:', err)
@@ -256,7 +263,7 @@ async function commitCreateCustomer(
await eventBus.emit({
type: 'customer.created',
- payload: { customer: data as Customer, userId },
+ payload: { customer: data as Customer, userId, companyId },
})
return { data: { customer_id: data.id } }
@@ -265,6 +272,7 @@ async function commitCreateCustomer(
async function commitCreateInvoice(
supabase: Awaited>,
userId: string,
+ companyId: string,
params: Record
): Promise<{ data?: Record; error?: string; status?: number }> {
const customerId = params.customer_id as string
@@ -281,7 +289,7 @@ async function commitCreateInvoice(
.from('customers')
.select('*')
.eq('id', customerId)
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
if (customerError || !customer) {
@@ -340,6 +348,7 @@ async function commitCreateInvoice(
.from('invoices')
.insert({
user_id: userId,
+ company_id: companyId,
customer_id: customerId,
invoice_number: invoiceNumber,
invoice_date: (params.invoice_date as string) || new Date().toISOString().split('T')[0],
@@ -406,7 +415,7 @@ async function commitCreateInvoice(
if (completeInvoice) {
await eventBus.emit({
type: 'invoice.created',
- payload: { invoice: completeInvoice as Invoice, userId },
+ payload: { invoice: completeInvoice as Invoice, userId, companyId },
})
}
@@ -416,6 +425,7 @@ async function commitCreateInvoice(
async function commitMarkInvoicePaid(
supabase: Awaited>,
userId: string,
+ companyId: string,
params: Record
): Promise<{ data?: Record; error?: string; status?: number }> {
const invoiceId = params.invoice_id as string
@@ -425,7 +435,7 @@ async function commitMarkInvoicePaid(
.from('invoices')
.select('*, customer:customers(*), items:invoice_items(*)')
.eq('id', invoiceId)
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
if (invoiceError || !invoice) return { error: 'Invoice not found', status: 404 }
@@ -436,7 +446,7 @@ async function commitMarkInvoicePaid(
const { data: settings } = await supabase
.from('company_settings')
.select('accounting_method, entity_type')
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
const accountingMethod = settings?.accounting_method || 'accrual'
@@ -447,12 +457,12 @@ async function commitMarkInvoicePaid(
if (isRealInvoice) {
if (accountingMethod === 'accrual') {
const je = await createInvoicePaymentJournalEntry(
- supabase, userId, invoice as Invoice, paymentDate, undefined, invoice.customer?.name
+ supabase, companyId, userId, invoice as Invoice, paymentDate, undefined, invoice.customer?.name
)
journalEntryId = je?.id ?? null
} else {
const je = await createInvoiceCashEntry(
- supabase, userId, invoice as Invoice, paymentDate, entityType, invoice.customer?.name
+ supabase, companyId, userId, invoice as Invoice, paymentDate, entityType, invoice.customer?.name
)
journalEntryId = je?.id ?? null
}
@@ -463,7 +473,7 @@ async function commitMarkInvoicePaid(
.from('invoices')
.update({ status: 'paid', paid_at: now, paid_amount: invoice.total })
.eq('id', invoiceId)
- .eq('user_id', userId)
+ .eq('company_id', companyId)
if (updateError) return { error: 'Failed to update invoice status', status: 500 }
@@ -473,6 +483,7 @@ async function commitMarkInvoicePaid(
async function commitSendInvoice(
supabase: Awaited>,
userId: string,
+ companyId: string,
params: Record,
userEmail?: string
): Promise<{ data?: Record; error?: string; status?: number }> {
@@ -487,7 +498,7 @@ async function commitSendInvoice(
.from('invoices')
.select('*, customer:customers(*), items:invoice_items(*)')
.eq('id', invoiceId)
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
if (invoiceError || !invoice) return { error: 'Invoice not found', status: 404 }
@@ -501,7 +512,7 @@ async function commitSendInvoice(
const { data: company, error: companyError } = await supabase
.from('company_settings')
.select('*')
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
if (companyError || !company) return { error: 'Company settings missing', status: 500 }
@@ -554,14 +565,14 @@ async function commitSendInvoice(
if (!result.success) return { error: `Failed to send email: ${result.error}`, status: 500 }
- await supabase.from('invoices').update({ status: 'sent' }).eq('id', invoiceId).eq('user_id', userId)
+ await supabase.from('invoices').update({ status: 'sent' }).eq('id', invoiceId).eq('company_id', companyId)
const isRealInvoice = !invoice.document_type || invoice.document_type === 'invoice'
let createdJournalEntryId: string | undefined
if (isRealInvoice && (company.accounting_method === 'accrual' || !company.accounting_method)) {
try {
const je = await createInvoiceJournalEntry(
- supabase, userId, invoice as Invoice, (company as CompanySettings).entity_type
+ supabase, companyId, userId, invoice as Invoice, (company as CompanySettings).entity_type
)
if (je) {
createdJournalEntryId = je.id
@@ -573,7 +584,7 @@ async function commitSendInvoice(
if (isRealInvoice) {
try {
const pdfArrayBuffer = new Uint8Array(pdfBuffer).buffer as ArrayBuffer
- await uploadDocument(supabase, userId, {
+ await uploadDocument(supabase, userId, companyId, {
name: filename,
buffer: pdfArrayBuffer,
type: 'application/pdf',
@@ -584,7 +595,7 @@ async function commitSendInvoice(
} catch { /* non-blocking */ }
}
- await eventBus.emit({ type: 'invoice.sent', payload: { invoice: invoice as Invoice, userId } })
+ await eventBus.emit({ type: 'invoice.sent', payload: { invoice: invoice as Invoice, userId, companyId } })
return { data: { message: `Invoice ${invoice.invoice_number} sent to ${customer.email}` } }
}
@@ -592,6 +603,7 @@ async function commitSendInvoice(
async function commitMarkInvoiceSent(
supabase: Awaited>,
userId: string,
+ companyId: string,
params: Record
): Promise<{ data?: Record; error?: string; status?: number }> {
const invoiceId = params.invoice_id as string
@@ -600,7 +612,7 @@ async function commitMarkInvoiceSent(
.from('invoices')
.select('*, customer:customers(*), items:invoice_items(*)')
.eq('id', invoiceId)
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
if (invoiceError || !invoice) return { error: 'Invoice not found', status: 404 }
@@ -610,14 +622,14 @@ async function commitMarkInvoiceSent(
.from('invoices')
.update({ status: 'sent' })
.eq('id', invoiceId)
- .eq('user_id', userId)
+ .eq('company_id', companyId)
if (updateError) return { error: 'Failed to update invoice status', status: 500 }
const { data: settings } = await supabase
.from('company_settings')
.select('accounting_method, entity_type')
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
const isRealInvoice = !invoice.document_type || invoice.document_type === 'invoice'
@@ -626,7 +638,7 @@ async function commitMarkInvoiceSent(
if (isRealInvoice && (settings?.accounting_method === 'accrual' || !settings?.accounting_method)) {
try {
const je = await createInvoiceJournalEntry(
- supabase, userId, invoice as Invoice,
+ supabase, companyId, userId, invoice as Invoice,
(settings?.entity_type as EntityType) || 'enskild_firma',
invoice.customer?.name
)
@@ -643,6 +655,7 @@ async function commitMarkInvoiceSent(
async function commitMatchTransactionInvoice(
supabase: Awaited>,
userId: string,
+ companyId: string,
params: Record
): Promise<{ data?: Record; error?: string; status?: number }> {
const transactionId = params.transaction_id as string
@@ -652,7 +665,7 @@ async function commitMatchTransactionInvoice(
.from('transactions')
.select('*')
.eq('id', transactionId)
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
if (txError || !transaction) return { error: 'Transaction not found', status: 404 }
@@ -663,7 +676,7 @@ async function commitMatchTransactionInvoice(
.from('invoices')
.select('*, customer:customers(*), items:invoice_items(*)')
.eq('id', invoiceId)
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
if (invError || !invoice) return { error: 'Invoice not found', status: 404 }
@@ -673,7 +686,7 @@ async function commitMatchTransactionInvoice(
// Storno conflicting journal entry
if (transaction.journal_entry_id) {
- await reverseEntry(supabase, userId, transaction.journal_entry_id)
+ await reverseEntry(supabase, companyId, userId, transaction.journal_entry_id)
await supabase.from('transactions').update({ journal_entry_id: null }).eq('id', transactionId)
}
@@ -688,7 +701,7 @@ async function commitMatchTransactionInvoice(
const { data: settings } = await supabase
.from('company_settings')
.select('accounting_method, entity_type')
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.single()
const accountingMethod = settings?.accounting_method || 'accrual'
@@ -698,12 +711,12 @@ async function commitMatchTransactionInvoice(
try {
if (accountingMethod === 'cash' && isFullyPaid) {
const je = await createInvoiceCashEntry(
- supabase, userId, invoice as Invoice, transaction.date, entityType, invoice.customer?.name
+ supabase, companyId, userId, invoice as Invoice, transaction.date, entityType, invoice.customer?.name
)
journalEntryId = je?.id ?? null
} else {
const je = await createInvoicePaymentJournalEntry(
- supabase, userId, invoice as Invoice, transaction.date, undefined, invoice.customer?.name, paidAmount
+ supabase, companyId, userId, invoice as Invoice, transaction.date, undefined, invoice.customer?.name, paidAmount
)
journalEntryId = je?.id ?? null
}
@@ -733,6 +746,7 @@ async function commitMatchTransactionInvoice(
await supabase.from('invoice_payments').insert({
user_id: userId,
+ company_id: companyId,
invoice_id: invoiceId,
payment_date: transaction.date,
amount: paidAmount,
@@ -757,7 +771,7 @@ async function commitMatchTransactionInvoice(
try {
await eventBus.emit({
type: 'invoice.match_confirmed',
- payload: { invoice: invoice as Invoice, transaction: transaction as Transaction, userId },
+ payload: { invoice: invoice as Invoice, transaction: transaction as Transaction, userId, companyId },
})
} catch { /* non-critical */ }
@@ -778,12 +792,14 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch the pending operation
const { data: op, error: fetchError } = await supabase
.from('pending_operations')
.select('*')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchError || !op) {
@@ -804,25 +820,25 @@ export async function POST(
switch (pendingOp.operation_type) {
case 'categorize_transaction':
- result = await commitCategorizeTransaction(supabase, user.id, pendingOp.params)
+ result = await commitCategorizeTransaction(supabase, user.id, companyId, pendingOp.params)
break
case 'create_customer':
- result = await commitCreateCustomer(supabase, user.id, pendingOp.params)
+ result = await commitCreateCustomer(supabase, user.id, companyId, pendingOp.params)
break
case 'create_invoice':
- result = await commitCreateInvoice(supabase, user.id, pendingOp.params)
+ result = await commitCreateInvoice(supabase, user.id, companyId, pendingOp.params)
break
case 'mark_invoice_paid':
- result = await commitMarkInvoicePaid(supabase, user.id, pendingOp.params)
+ result = await commitMarkInvoicePaid(supabase, user.id, companyId, pendingOp.params)
break
case 'send_invoice':
- result = await commitSendInvoice(supabase, user.id, pendingOp.params, user.email)
+ result = await commitSendInvoice(supabase, user.id, companyId, pendingOp.params, user.email)
break
case 'mark_invoice_sent':
- result = await commitMarkInvoiceSent(supabase, user.id, pendingOp.params)
+ result = await commitMarkInvoiceSent(supabase, user.id, companyId, pendingOp.params)
break
case 'match_transaction_invoice':
- result = await commitMatchTransactionInvoice(supabase, user.id, pendingOp.params)
+ result = await commitMatchTransactionInvoice(supabase, user.id, companyId, pendingOp.params)
break
default:
return NextResponse.json({ error: 'Unknown operation type' }, { status: 400 })
diff --git a/app/api/pending-operations/[id]/reject/__tests__/route.test.ts b/app/api/pending-operations/[id]/reject/__tests__/route.test.ts
index 3fa9c6fd..a779c591 100644
--- a/app/api/pending-operations/[id]/reject/__tests__/route.test.ts
+++ b/app/api/pending-operations/[id]/reject/__tests__/route.test.ts
@@ -11,6 +11,11 @@ vi.mock('@/lib/supabase/server', () => ({
createClient: () => Promise.resolve(mockSupabase),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
import { POST } from '../../reject/route'
describe('POST /api/pending-operations/:id/reject', () => {
diff --git a/app/api/pending-operations/[id]/reject/route.ts b/app/api/pending-operations/[id]/reject/route.ts
index 591084bb..f2a7a8f4 100644
--- a/app/api/pending-operations/[id]/reject/route.ts
+++ b/app/api/pending-operations/[id]/reject/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
/**
* POST /api/pending-operations/:id/reject
@@ -18,11 +19,13 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data: op, error: fetchError } = await supabase
.from('pending_operations')
.select('id, status')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchError || !op) {
diff --git a/app/api/pending-operations/__tests__/route.test.ts b/app/api/pending-operations/__tests__/route.test.ts
index 65a99d1c..889e9d82 100644
--- a/app/api/pending-operations/__tests__/route.test.ts
+++ b/app/api/pending-operations/__tests__/route.test.ts
@@ -10,6 +10,11 @@ vi.mock('@/lib/supabase/server', () => ({
createClient: () => Promise.resolve(mockSupabase),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
import { GET } from '../route'
describe('GET /api/pending-operations', () => {
diff --git a/app/api/pending-operations/route.ts b/app/api/pending-operations/route.ts
index ead138ed..6a250242 100644
--- a/app/api/pending-operations/route.ts
+++ b/app/api/pending-operations/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { validateQuery } from '@/lib/api/validate'
import { PendingOperationsQuerySchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
/**
* GET /api/pending-operations
@@ -16,6 +17,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const result = validateQuery(request, PendingOperationsQuerySchema)
if (!result.success) return result.response
const { status, limit, offset } = result.data
@@ -23,7 +26,7 @@ export async function GET(request: Request) {
const { data, error, count } = await supabase
.from('pending_operations')
.select('*', { count: 'exact' })
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('status', status)
.order('created_at', { ascending: false })
.range(offset, offset + limit - 1)
diff --git a/app/api/reconciliation/bank/link/route.ts b/app/api/reconciliation/bank/link/route.ts
index 866fda7e..a767110c 100644
--- a/app/api/reconciliation/bank/link/route.ts
+++ b/app/api/reconciliation/bank/link/route.ts
@@ -4,6 +4,7 @@ import { ensureInitialized } from '@/lib/init'
import { manualLink } from '@/lib/reconciliation/bank-reconciliation'
import { validateBody } from '@/lib/api/validate'
import { BankLinkSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
ensureInitialized()
@@ -15,11 +16,13 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, BankLinkSchema)
if (!validation.success) return validation.response
const { transaction_id, journal_entry_id } = validation.data
- const result = await manualLink(supabase, user.id, transaction_id, journal_entry_id)
+ const result = await manualLink(supabase, companyId, transaction_id, journal_entry_id)
if (!result.success) {
return NextResponse.json({ error: result.error }, { status: 400 })
diff --git a/app/api/reconciliation/bank/run/route.ts b/app/api/reconciliation/bank/run/route.ts
index 5247b783..c808202f 100644
--- a/app/api/reconciliation/bank/run/route.ts
+++ b/app/api/reconciliation/bank/run/route.ts
@@ -4,6 +4,7 @@ import { ensureInitialized } from '@/lib/init'
import { runReconciliation } from '@/lib/reconciliation/bank-reconciliation'
import { validateBody } from '@/lib/api/validate'
import { RunReconciliationSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
ensureInitialized()
@@ -15,11 +16,13 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, RunReconciliationSchema)
if (!validation.success) return validation.response
const { date_from, date_to, dry_run } = validation.data
- const result = await runReconciliation(supabase, user.id, {
+ const result = await runReconciliation(supabase, companyId, {
dateFrom: date_from,
dateTo: date_to,
dryRun: dry_run ?? false,
diff --git a/app/api/reconciliation/bank/status/route.ts b/app/api/reconciliation/bank/status/route.ts
index 0928b649..7d9b9807 100644
--- a/app/api/reconciliation/bank/status/route.ts
+++ b/app/api/reconciliation/bank/status/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { getReconciliationStatus } from '@/lib/reconciliation/bank-reconciliation'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -10,11 +11,13 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const dateFrom = searchParams.get('date_from') || undefined
const dateTo = searchParams.get('date_to') || undefined
- const status = await getReconciliationStatus(supabase, user.id, dateFrom, dateTo)
+ const status = await getReconciliationStatus(supabase, companyId, dateFrom, dateTo)
return NextResponse.json({ data: status })
}
diff --git a/app/api/reconciliation/bank/unlink/route.ts b/app/api/reconciliation/bank/unlink/route.ts
index e2e10a9c..6a0cd16e 100644
--- a/app/api/reconciliation/bank/unlink/route.ts
+++ b/app/api/reconciliation/bank/unlink/route.ts
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server'
import { unlinkReconciliation } from '@/lib/reconciliation/bank-reconciliation'
import { validateBody } from '@/lib/api/validate'
import { BankUnlinkSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
export async function POST(request: Request) {
const supabase = await createClient()
@@ -12,11 +13,13 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, BankUnlinkSchema)
if (!validation.success) return validation.response
const { transaction_id } = validation.data
- const result = await unlinkReconciliation(supabase, user.id, transaction_id)
+ const result = await unlinkReconciliation(supabase, companyId, transaction_id)
if (!result.success) {
return NextResponse.json({ error: result.error }, { status: 400 })
diff --git a/app/api/reconciliation/bank/unmatched-entries/route.ts b/app/api/reconciliation/bank/unmatched-entries/route.ts
index 10a102c4..47708296 100644
--- a/app/api/reconciliation/bank/unmatched-entries/route.ts
+++ b/app/api/reconciliation/bank/unmatched-entries/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { fetchUnlinkedGLLines } from '@/lib/reconciliation/bank-reconciliation'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -10,11 +11,13 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const dateFrom = searchParams.get('date_from') || undefined
const dateTo = searchParams.get('date_to') || undefined
- const lines = await fetchUnlinkedGLLines(supabase, user.id, dateFrom, dateTo)
+ const lines = await fetchUnlinkedGLLines(supabase, companyId, dateFrom, dateTo)
return NextResponse.json({ data: lines })
}
diff --git a/app/api/reports/ar-ledger/route.ts b/app/api/reports/ar-ledger/route.ts
index c5a8318b..de1492f4 100644
--- a/app/api/reports/ar-ledger/route.ts
+++ b/app/api/reports/ar-ledger/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateARLedger } from '@/lib/reports/ar-ledger'
import { generateARReconciliation } from '@/lib/reports/ar-reconciliation'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -12,15 +13,17 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const asOfDate = searchParams.get('as_of_date') || undefined
const periodId = searchParams.get('period_id') || undefined
- const ledger = await generateARLedger(supabase, user.id, asOfDate)
+ const ledger = await generateARLedger(supabase, companyId, asOfDate)
let reconciliation = null
if (periodId) {
- reconciliation = await generateARReconciliation(supabase, user.id, periodId)
+ reconciliation = await generateARReconciliation(supabase, companyId, periodId)
}
return NextResponse.json({
diff --git a/app/api/reports/audit-trail/__tests__/route.test.ts b/app/api/reports/audit-trail/__tests__/route.test.ts
index 99a19e70..675b6521 100644
--- a/app/api/reports/audit-trail/__tests__/route.test.ts
+++ b/app/api/reports/audit-trail/__tests__/route.test.ts
@@ -9,6 +9,11 @@ vi.mock('@/lib/core/audit/audit-service', () => ({
getAuditLog: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
import { createClient } from '@/lib/supabase/server'
import { getAuditLog } from '@/lib/core/audit/audit-service'
import { GET } from '../route'
diff --git a/app/api/reports/audit-trail/route.ts b/app/api/reports/audit-trail/route.ts
index e11dc47f..fe733535 100644
--- a/app/api/reports/audit-trail/route.ts
+++ b/app/api/reports/audit-trail/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { getAuditLog } from '@/lib/core/audit/audit-service'
+import { requireCompanyId } from '@/lib/company/context'
import type { AuditLogEntry, AuditAction } from '@/types'
const CSV_HEADERS = 'timestamp,action,table_name,record_id,description,old_state,new_state'
@@ -34,6 +35,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const format = searchParams.get('format') || 'json'
@@ -52,7 +55,7 @@ export async function GET(request: Request) {
const pageSize = 500
while (true) {
- const result = await getAuditLog(supabase, user.id, {
+ const result = await getAuditLog(supabase, companyId, {
...filters,
page,
pageSize,
diff --git a/app/api/reports/balance-sheet/route.ts b/app/api/reports/balance-sheet/route.ts
index aaf9d62a..b7bdf70a 100644
--- a/app/api/reports/balance-sheet/route.ts
+++ b/app/api/reports/balance-sheet/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateBalanceSheet } from '@/lib/reports/balance-sheet'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -10,6 +11,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
@@ -21,11 +24,11 @@ export async function GET(request: Request) {
.from('fiscal_periods')
.select('period_start, period_end')
.eq('id', periodId)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
try {
- const result = await generateBalanceSheet(supabase, user.id, periodId)
+ const result = await generateBalanceSheet(supabase, companyId, periodId)
if (period) {
result.period = {
diff --git a/app/api/reports/full-archive/route.ts b/app/api/reports/full-archive/route.ts
index 57f4d478..19eb7cf3 100644
--- a/app/api/reports/full-archive/route.ts
+++ b/app/api/reports/full-archive/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateFullArchive } from '@/lib/reports/full-archive-export'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -10,6 +11,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
@@ -18,7 +21,7 @@ export async function GET(request: Request) {
}
try {
- const zipBuffer = await generateFullArchive(supabase, user.id, {
+ const zipBuffer = await generateFullArchive(supabase, companyId, {
period_id: periodId,
include_documents: searchParams.get('include_documents') !== 'false',
})
diff --git a/app/api/reports/general-ledger/route.ts b/app/api/reports/general-ledger/route.ts
index 70d4fc11..e48e9d39 100644
--- a/app/api/reports/general-ledger/route.ts
+++ b/app/api/reports/general-ledger/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateGeneralLedger } from '@/lib/reports/general-ledger'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -11,6 +12,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
const accountFrom = searchParams.get('account_from') || undefined
@@ -20,7 +23,7 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'period_id is required' }, { status: 400 })
}
- const data = await generateGeneralLedger(supabase, user.id, periodId, accountFrom, accountTo)
+ const data = await generateGeneralLedger(supabase, companyId, periodId, accountFrom, accountTo)
return NextResponse.json({ data })
}
diff --git a/app/api/reports/income-statement/route.ts b/app/api/reports/income-statement/route.ts
index 4005669d..ed15db2f 100644
--- a/app/api/reports/income-statement/route.ts
+++ b/app/api/reports/income-statement/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateIncomeStatement } from '@/lib/reports/income-statement'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -10,6 +11,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
@@ -22,11 +25,11 @@ export async function GET(request: Request) {
.from('fiscal_periods')
.select('period_start, period_end')
.eq('id', periodId)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
try {
- const result = await generateIncomeStatement(supabase, user.id, periodId)
+ const result = await generateIncomeStatement(supabase, companyId, periodId)
if (period) {
result.period = {
diff --git a/app/api/reports/ink2/route.ts b/app/api/reports/ink2/route.ts
index efcbcdc9..42af7e57 100644
--- a/app/api/reports/ink2/route.ts
+++ b/app/api/reports/ink2/route.ts
@@ -6,6 +6,7 @@ import {
sruFileToString,
getSRUFilename,
} from '@/lib/reports/ink2/sru-generator'
+import { requireCompanyId } from '@/lib/company/context'
/**
* GET /api/reports/ink2
@@ -24,6 +25,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
const format = searchParams.get('format') || 'json'
@@ -36,7 +39,7 @@ export async function GET(request: Request) {
}
try {
- const declaration = await generateINK2Declaration(supabase, user.id, periodId)
+ const declaration = await generateINK2Declaration(supabase, companyId, periodId)
if (format === 'sru') {
const sruFile = generateSRUFile(declaration)
diff --git a/app/api/reports/journal-register/route.ts b/app/api/reports/journal-register/route.ts
index db7365f0..ad025158 100644
--- a/app/api/reports/journal-register/route.ts
+++ b/app/api/reports/journal-register/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateJournalRegister } from '@/lib/reports/journal-register'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -11,6 +12,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
@@ -18,7 +21,7 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'period_id is required' }, { status: 400 })
}
- const data = await generateJournalRegister(supabase, user.id, periodId)
+ const data = await generateJournalRegister(supabase, companyId, periodId)
return NextResponse.json({ data })
}
diff --git a/app/api/reports/kpi/route.ts b/app/api/reports/kpi/route.ts
index 3ce36dee..a0691121 100644
--- a/app/api/reports/kpi/route.ts
+++ b/app/api/reports/kpi/route.ts
@@ -11,6 +11,7 @@ import {
calculateAvgPaymentDays,
} from '@/lib/reports/kpi'
import { mergeWithDefaults } from '@/lib/reports/kpi-definitions'
+import { requireCompanyId } from '@/lib/company/context'
import type { KPIReport, KPIPreferences } from '@/types'
export async function GET(request: Request) {
@@ -18,6 +19,8 @@ export async function GET(request: Request) {
const { data: { user } } = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
if (!periodId) {
@@ -28,7 +31,7 @@ export async function GET(request: Request) {
.from('fiscal_periods')
.select('*')
.eq('id', periodId)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (periodError || !period) {
@@ -39,7 +42,7 @@ export async function GET(request: Request) {
const { data: prefsData } = await supabase
.from('extension_data')
.select('value')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('extension_id', 'core/kpi')
.eq('key', 'preferences')
.single()
@@ -50,14 +53,14 @@ export async function GET(request: Request) {
const [incomeStatement, trialBalanceResult, arLedger, monthlyBreakdown, paidInvoicesResult] =
await Promise.all([
- generateIncomeStatement(supabase, user.id, periodId),
- generateTrialBalance(supabase, user.id, periodId),
- generateARLedger(supabase, user.id),
- generateMonthlyBreakdown(supabase, user.id, periodId),
+ generateIncomeStatement(supabase, companyId, periodId),
+ generateTrialBalance(supabase, companyId, periodId),
+ generateARLedger(supabase, companyId),
+ generateMonthlyBreakdown(supabase, companyId, periodId),
supabase
.from('invoices')
.select('invoice_date, paid_at')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('status', 'paid')
.not('paid_at', 'is', null),
])
diff --git a/app/api/reports/monthly-breakdown/route.ts b/app/api/reports/monthly-breakdown/route.ts
index db3fc9c9..295cc375 100644
--- a/app/api/reports/monthly-breakdown/route.ts
+++ b/app/api/reports/monthly-breakdown/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateMonthlyBreakdown } from '@/lib/reports/monthly-breakdown'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -10,6 +11,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
@@ -18,7 +21,7 @@ export async function GET(request: Request) {
}
try {
- const data = await generateMonthlyBreakdown(supabase, user.id, periodId)
+ const data = await generateMonthlyBreakdown(supabase, companyId, periodId)
return NextResponse.json({ data })
} catch {
return NextResponse.json({ error: 'Failed to generate monthly breakdown' }, { status: 500 })
diff --git a/app/api/reports/ne-bilaga/route.ts b/app/api/reports/ne-bilaga/route.ts
index 87548f36..a8cbc870 100644
--- a/app/api/reports/ne-bilaga/route.ts
+++ b/app/api/reports/ne-bilaga/route.ts
@@ -6,6 +6,7 @@ import {
sruFileToString,
getSRUFilename,
} from '@/lib/reports/ne-bilaga/sru-generator'
+import { requireCompanyId } from '@/lib/company/context'
/**
* GET /api/reports/ne-bilaga
@@ -28,6 +29,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
const format = searchParams.get('format') || 'json'
@@ -40,7 +43,7 @@ export async function GET(request: Request) {
}
try {
- const declaration = await generateNEDeclaration(supabase, user.id, periodId)
+ const declaration = await generateNEDeclaration(supabase, companyId, periodId)
if (format === 'sru') {
// Generate and return SRU file
diff --git a/app/api/reports/sie-export/route.ts b/app/api/reports/sie-export/route.ts
index dcdb2e02..34b7643e 100644
--- a/app/api/reports/sie-export/route.ts
+++ b/app/api/reports/sie-export/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateSIEExport } from '@/lib/reports/sie-export'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -10,6 +11,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
@@ -21,7 +24,7 @@ export async function GET(request: Request) {
const { data: company } = await supabase
.from('company_settings')
.select('company_name, org_number')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (!company) {
@@ -29,7 +32,7 @@ export async function GET(request: Request) {
}
try {
- const sieContent = await generateSIEExport(supabase, user.id, {
+ const sieContent = await generateSIEExport(supabase, companyId, {
fiscal_period_id: periodId,
company_name: company.company_name || 'Unknown',
org_number: company.org_number,
diff --git a/app/api/reports/supplier-ledger/route.ts b/app/api/reports/supplier-ledger/route.ts
index e0ddc6a3..6bae0496 100644
--- a/app/api/reports/supplier-ledger/route.ts
+++ b/app/api/reports/supplier-ledger/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateSupplierLedger } from '@/lib/reports/supplier-ledger'
import { generateReconciliation } from '@/lib/reports/supplier-reconciliation'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -12,15 +13,17 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const asOfDate = searchParams.get('as_of_date') || undefined
const periodId = searchParams.get('period_id') || undefined
- const ledger = await generateSupplierLedger(supabase, user.id, asOfDate)
+ const ledger = await generateSupplierLedger(supabase, companyId, asOfDate)
let reconciliation = null
if (periodId) {
- reconciliation = await generateReconciliation(supabase, user.id, periodId)
+ reconciliation = await generateReconciliation(supabase, companyId, periodId)
}
return NextResponse.json({
diff --git a/app/api/reports/trial-balance/route.ts b/app/api/reports/trial-balance/route.ts
index fb2b9daf..5aeee60e 100644
--- a/app/api/reports/trial-balance/route.ts
+++ b/app/api/reports/trial-balance/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateTrialBalance } from '@/lib/reports/trial-balance'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(request: Request) {
const supabase = await createClient()
@@ -10,6 +11,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
@@ -18,7 +21,7 @@ export async function GET(request: Request) {
}
try {
- const result = await generateTrialBalance(supabase, user.id, periodId)
+ const result = await generateTrialBalance(supabase, companyId, periodId)
return NextResponse.json({ data: result })
} catch (err) {
return NextResponse.json(
diff --git a/app/api/reports/vat-declaration/route.ts b/app/api/reports/vat-declaration/route.ts
index 8f1a2162..c4eb850a 100644
--- a/app/api/reports/vat-declaration/route.ts
+++ b/app/api/reports/vat-declaration/route.ts
@@ -4,6 +4,7 @@ import {
calculateVatDeclaration,
formatPeriodLabel,
} from '@/lib/reports/vat-declaration'
+import { requireCompanyId } from '@/lib/company/context'
import type { VatPeriodType, AccountingMethod } from '@/types'
/**
@@ -29,6 +30,8 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const periodType = searchParams.get('periodType') as VatPeriodType | null
const yearStr = searchParams.get('year')
@@ -94,7 +97,7 @@ export async function GET(request: Request) {
const { data: settings } = await supabase
.from('company_settings')
.select('accounting_method')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const accountingMethod = (settings?.accounting_method as AccountingMethod) || 'accrual'
@@ -102,7 +105,7 @@ export async function GET(request: Request) {
try {
const declaration = await calculateVatDeclaration(
supabase,
- user.id,
+ companyId,
periodType,
year,
period,
diff --git a/app/api/sandbox/seed/route.ts b/app/api/sandbox/seed/route.ts
index 308b1683..e643c923 100644
--- a/app/api/sandbox/seed/route.ts
+++ b/app/api/sandbox/seed/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
/**
* POST /api/sandbox/seed
@@ -18,11 +19,13 @@ export async function POST() {
return NextResponse.json({ error: 'Sandbox is only available for anonymous users' }, { status: 403 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Idempotency: if already seeded, return early
const { data: existing } = await supabase
.from('company_settings')
.select('id')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.maybeSingle()
if (existing) {
@@ -43,6 +46,7 @@ export async function POST() {
.from('company_settings')
.insert({
user_id: userId,
+ company_id: companyId,
entity_type: 'enskild_firma',
company_name: 'Sandlådan Konsult',
org_number: '199001011234',
@@ -80,6 +84,7 @@ export async function POST() {
.from('fiscal_periods')
.insert({
user_id: userId,
+ company_id: companyId,
name: `Räkenskapsår ${currentYear}`,
period_start: `${currentYear}-01-01`,
period_end: `${currentYear}-12-31`,
@@ -95,6 +100,7 @@ export async function POST() {
.insert([
{
user_id: userId,
+ company_id: companyId,
name: 'Björk & Partner AB',
customer_type: 'swedish_business',
email: 'faktura@bjorkpartner.se',
@@ -109,6 +115,7 @@ export async function POST() {
},
{
user_id: userId,
+ company_id: companyId,
name: 'Schmidt GmbH',
customer_type: 'eu_business',
email: 'billing@schmidt.de',
@@ -123,6 +130,7 @@ export async function POST() {
},
{
user_id: userId,
+ company_id: companyId,
name: 'Anna Lindström',
customer_type: 'individual',
email: 'anna.lindstrom@example.com',
@@ -158,6 +166,7 @@ export async function POST() {
.insert([
{
user_id: userId,
+ company_id: companyId,
customer_id: customerMap['Björk & Partner AB'],
invoice_number: 'F-2026001',
invoice_date: toDateStr(thirtyDaysAgo),
@@ -175,6 +184,7 @@ export async function POST() {
},
{
user_id: userId,
+ company_id: companyId,
customer_id: customerMap['Schmidt GmbH'],
invoice_number: 'F-2026002',
invoice_date: toDateStr(fifteenDaysAgo),
@@ -190,6 +200,7 @@ export async function POST() {
},
{
user_id: userId,
+ company_id: companyId,
customer_id: customerMap['Anna Lindström'],
invoice_number: 'F-2026003',
invoice_date: toDateStr(thirtyDaysAgo),
@@ -205,6 +216,7 @@ export async function POST() {
},
{
user_id: userId,
+ company_id: companyId,
customer_id: customerMap['Björk & Partner AB'],
invoice_number: 'F-2026004',
invoice_date: toDateStr(today),
@@ -273,7 +285,7 @@ export async function POST() {
const { data: accounts } = await supabase
.from('chart_of_accounts')
.select('id, account_number')
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.in('account_number', ['1510', '1930', '2611', '3001'])
const accountMap = Object.fromEntries(
@@ -291,6 +303,7 @@ export async function POST() {
.from('journal_entries')
.insert({
user_id: userId,
+ company_id: companyId,
fiscal_period_id: fiscalPeriod.id,
voucher_number: voucherNum1 ?? 1,
voucher_series: 'A',
@@ -316,6 +329,7 @@ export async function POST() {
.from('journal_entries')
.insert({
user_id: userId,
+ company_id: companyId,
fiscal_period_id: fiscalPeriod.id,
voucher_number: voucherNum2 ?? 2,
voucher_series: 'A',
@@ -388,6 +402,7 @@ export async function POST() {
// Categorized expenses
{
user_id: userId,
+ company_id: companyId,
date: toDateStr(thirtyDaysAgo),
description: 'CLAS OHLSON STOCKHOLM',
amount: -450,
@@ -397,6 +412,7 @@ export async function POST() {
},
{
user_id: userId,
+ company_id: companyId,
date: toDateStr(fifteenDaysAgo),
description: 'GITHUB INC',
amount: -999,
@@ -406,6 +422,7 @@ export async function POST() {
},
{
user_id: userId,
+ company_id: companyId,
date: toDateStr(fiveDaysAgo),
description: 'SJ BILJETT',
amount: -2500,
@@ -416,6 +433,7 @@ export async function POST() {
// Income matched to paid invoice
{
user_id: userId,
+ company_id: companyId,
date: toDateStr(fifteenDaysAgo),
description: 'BJÖRK & PARTNER AB BETALNING F-2026001',
amount: 18750,
@@ -428,6 +446,7 @@ export async function POST() {
// Private transaction
{
user_id: userId,
+ company_id: companyId,
date: toDateStr(fiveDaysAgo),
description: 'PRIVAT INSÄTTNING',
amount: 5000,
@@ -437,6 +456,7 @@ export async function POST() {
// Uncategorized transactions
{
user_id: userId,
+ company_id: companyId,
date: toDateStr(fiveDaysAgo),
description: 'SWISH BETALNING 0701234567',
amount: -350,
@@ -445,6 +465,7 @@ export async function POST() {
},
{
user_id: userId,
+ company_id: companyId,
date: toDateStr(today),
description: 'INSÄTTNING BANKGIRO',
amount: 1200,
@@ -453,6 +474,7 @@ export async function POST() {
},
{
user_id: userId,
+ company_id: companyId,
date: toDateStr(today),
description: 'KORTBETALNING RESTAURANG',
amount: -680,
@@ -473,6 +495,7 @@ export async function POST() {
.insert([
{
user_id: userId,
+ company_id: companyId,
title: 'Momsdeklaration Q1 2026',
due_date: toDateStr(momsDeadline),
deadline_type: 'tax',
@@ -485,6 +508,7 @@ export async function POST() {
},
{
user_id: userId,
+ company_id: companyId,
title: 'Inkomstdeklaration 2025',
due_date: `${currentYear}-05-02`,
deadline_type: 'tax',
diff --git a/app/api/settings/api-keys/[id]/route.ts b/app/api/settings/api-keys/[id]/route.ts
index 9caa5ec7..0d0e0e6d 100644
--- a/app/api/settings/api-keys/[id]/route.ts
+++ b/app/api/settings/api-keys/[id]/route.ts
@@ -1,5 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
/**
* DELETE /api/settings/api-keys/[id] — Revoke an API key (soft delete)
@@ -16,11 +17,13 @@ export async function DELETE(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { error } = await supabase
.from('api_keys')
.update({ revoked_at: new Date().toISOString() })
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.is('revoked_at', null)
if (error) {
diff --git a/app/api/settings/api-keys/route.ts b/app/api/settings/api-keys/route.ts
index fb2c52d4..3dae6b34 100644
--- a/app/api/settings/api-keys/route.ts
+++ b/app/api/settings/api-keys/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateApiKey, hashApiKey, DEFAULT_SCOPES, validateScopes } from '@/lib/auth/api-keys'
+import { requireCompanyId } from '@/lib/company/context'
import type { ApiKeyScope } from '@/lib/auth/api-keys'
/**
@@ -14,10 +15,12 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('api_keys')
.select('id, key_prefix, name, scopes, rate_limit_rpm, last_used_at, revoked_at, created_at')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.order('created_at', { ascending: false })
if (error) {
@@ -39,6 +42,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
let name = 'Unnamed key'
let scopes: ApiKeyScope[] = DEFAULT_SCOPES
try {
@@ -54,11 +59,11 @@ export async function POST(request: Request) {
// Empty body is fine, use defaults
}
- // Limit to 10 active keys per user
+ // Limit to 10 active keys per company
const { count } = await supabase
.from('api_keys')
.select('id', { count: 'exact', head: true })
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.is('revoked_at', null)
if (count !== null && count >= 10) {
@@ -74,6 +79,7 @@ export async function POST(request: Request) {
.from('api_keys')
.insert({
user_id: user.id,
+ company_id: companyId,
key_hash: hash,
key_prefix: prefix,
name,
diff --git a/app/api/settings/counterparty-templates/route.ts b/app/api/settings/counterparty-templates/route.ts
index c09db71f..739c7587 100644
--- a/app/api/settings/counterparty-templates/route.ts
+++ b/app/api/settings/counterparty-templates/route.ts
@@ -1,15 +1,18 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
+import { requireCompanyId } from '@/lib/company/context'
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 companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('categorization_templates')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('is_active', true)
.order('occurrence_count', { ascending: false })
@@ -23,6 +26,8 @@ export async function DELETE(request: Request) {
const { data: { user } } = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ const companyId = await requireCompanyId(supabase, user.id)
+
let id: string | undefined
try {
const body = await request.json()
@@ -36,7 +41,7 @@ export async function DELETE(request: Request) {
.from('categorization_templates')
.update({ is_active: false })
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (error) return NextResponse.json({ error: error.message }, { status: 500 })
diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts
index 98abad47..13bd0378 100644
--- a/app/api/settings/route.ts
+++ b/app/api/settings/route.ts
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server'
import { didTaxFieldsChange, regenerateTaxDeadlinesForUser } from '@/lib/tax/deadline-generator'
import { validateBody } from '@/lib/api/validate'
import { UpdateSettingsSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET() {
const supabase = await createClient()
@@ -13,10 +14,12 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('company_settings')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (error) {
@@ -35,11 +38,13 @@ export async function PUT(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch current settings to check for tax-relevant changes
const { data: oldSettings } = await supabase
.from('company_settings')
.select('entity_type, moms_period, f_skatt, vat_registered, pays_salaries, fiscal_year_start_month, onboarding_complete')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const validation = await validateBody(request, UpdateSettingsSchema)
@@ -65,7 +70,7 @@ export async function PUT(request: Request) {
const { data, error } = await supabase
.from('company_settings')
.update(body)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.select()
.single()
@@ -76,7 +81,7 @@ export async function PUT(request: Request) {
// Check if tax-relevant fields changed and regenerate deadlines
if (oldSettings && didTaxFieldsChange(oldSettings, data)) {
try {
- await regenerateTaxDeadlinesForUser(supabase, user.id, {
+ await regenerateTaxDeadlinesForUser(supabase, companyId, {
entity_type: data.entity_type,
moms_period: data.moms_period,
f_skatt: data.f_skatt,
diff --git a/app/api/supplier-invoices/[id]/approve/__tests__/route.test.ts b/app/api/supplier-invoices/[id]/approve/__tests__/route.test.ts
index c1666595..0cde102e 100644
--- a/app/api/supplier-invoices/[id]/approve/__tests__/route.test.ts
+++ b/app/api/supplier-invoices/[id]/approve/__tests__/route.test.ts
@@ -16,6 +16,11 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
import { eventBus } from '@/lib/events'
import { POST } from '../route'
diff --git a/app/api/supplier-invoices/[id]/approve/route.ts b/app/api/supplier-invoices/[id]/approve/route.ts
index 30dd3c04..151c70e7 100644
--- a/app/api/supplier-invoices/[id]/approve/route.ts
+++ b/app/api/supplier-invoices/[id]/approve/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { eventBus } from '@/lib/events'
import { ensureInitialized } from '@/lib/init'
+import { requireCompanyId } from '@/lib/company/context'
import type { SupplierInvoice } from '@/types'
ensureInitialized()
@@ -19,11 +20,13 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data: invoice } = await supabase
.from('supplier_invoices')
.select('*')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (!invoice) {
@@ -41,7 +44,7 @@ export async function POST(
.from('supplier_invoices')
.update({ status: 'approved' })
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.select()
.single()
@@ -52,7 +55,7 @@ export async function POST(
try {
await eventBus.emit({
type: 'supplier_invoice.approved',
- payload: { supplierInvoice: data as SupplierInvoice, userId: user.id },
+ payload: { supplierInvoice: data as SupplierInvoice, companyId, userId: user.id },
})
} catch {
// Non-blocking
diff --git a/app/api/supplier-invoices/[id]/credit/route.ts b/app/api/supplier-invoices/[id]/credit/route.ts
index 43989e3a..bcff6750 100644
--- a/app/api/supplier-invoices/[id]/credit/route.ts
+++ b/app/api/supplier-invoices/[id]/credit/route.ts
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server'
import { eventBus } from '@/lib/events'
import { ensureInitialized } from '@/lib/init'
import { createSupplierCreditNoteEntry } from '@/lib/bookkeeping/supplier-invoice-entries'
+import { requireCompanyId } from '@/lib/company/context'
import type { SupplierInvoice, SupplierInvoiceItem, AccountingMethod } from '@/types'
ensureInitialized()
@@ -20,12 +21,14 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch original invoice with supplier and items
const { data: original, error: fetchError } = await supabase
.from('supplier_invoices')
.select('*, supplier:suppliers(*), items:supplier_invoice_items(*)')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchError || !original) {
@@ -48,6 +51,7 @@ export async function POST(
.from('supplier_invoices')
.insert({
user_id: user.id,
+ company_id: companyId,
supplier_id: original.supplier_id,
arrival_number: arrivalNum,
supplier_invoice_number: `KREDIT-${original.supplier_invoice_number}`,
@@ -96,7 +100,7 @@ export async function POST(
const { data: settings } = await supabase
.from('company_settings')
.select('accounting_method')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const accountingMethod = (settings?.accounting_method as AccountingMethod) || 'accrual'
@@ -108,6 +112,7 @@ export async function POST(
try {
const journalEntry = await createSupplierCreditNoteEntry(
supabase,
+ companyId,
user.id,
creditNote as SupplierInvoice,
creditItems as SupplierInvoiceItem[],
@@ -144,6 +149,7 @@ export async function POST(
payload: {
supplierInvoice: original as SupplierInvoice,
creditNote: creditNote as SupplierInvoice,
+ companyId,
userId: user.id,
},
})
diff --git a/app/api/supplier-invoices/[id]/mark-paid/__tests__/route.test.ts b/app/api/supplier-invoices/[id]/mark-paid/__tests__/route.test.ts
index 4692cdb5..e065d43d 100644
--- a/app/api/supplier-invoices/[id]/mark-paid/__tests__/route.test.ts
+++ b/app/api/supplier-invoices/[id]/mark-paid/__tests__/route.test.ts
@@ -17,6 +17,11 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
const mockCreateSupplierInvoicePaymentEntry = vi.fn()
const mockCreateSupplierInvoiceCashEntry = vi.fn()
vi.mock('@/lib/bookkeeping/supplier-invoice-entries', () => ({
diff --git a/app/api/supplier-invoices/[id]/mark-paid/route.ts b/app/api/supplier-invoices/[id]/mark-paid/route.ts
index c9b0b184..7915c8e3 100644
--- a/app/api/supplier-invoices/[id]/mark-paid/route.ts
+++ b/app/api/supplier-invoices/[id]/mark-paid/route.ts
@@ -8,6 +8,7 @@ import {
} from '@/lib/bookkeeping/supplier-invoice-entries'
import { validateBody } from '@/lib/api/validate'
import { MarkSupplierInvoicePaidSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
import type { SupplierInvoice, SupplierInvoiceItem } from '@/types'
ensureInitialized()
@@ -25,6 +26,8 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, MarkSupplierInvoicePaidSchema)
if (!validation.success) return validation.response
const body = validation.data
@@ -34,7 +37,7 @@ export async function POST(
.from('supplier_invoices')
.select('*, supplier:suppliers(*), items:supplier_invoice_items(*)')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchError || !invoice) {
@@ -56,7 +59,7 @@ export async function POST(
const { data: settings } = await supabase
.from('company_settings')
.select('accounting_method')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const accountingMethod = settings?.accounting_method || 'accrual'
@@ -68,6 +71,7 @@ export async function POST(
if (accountingMethod === 'cash') {
const journalEntry = await createSupplierInvoiceCashEntry(
supabase,
+ companyId,
user.id,
invoice as SupplierInvoice,
(invoice.items || []) as SupplierInvoiceItem[],
@@ -79,6 +83,7 @@ export async function POST(
} else {
const journalEntry = await createSupplierInvoicePaymentEntry(
supabase,
+ companyId,
user.id,
invoice as SupplierInvoice,
paymentAmount,
@@ -134,7 +139,7 @@ export async function POST(
try {
await eventBus.emit({
type: 'supplier_invoice.paid',
- payload: { supplierInvoice: invoice as SupplierInvoice, paymentAmount, userId: user.id },
+ payload: { supplierInvoice: invoice as SupplierInvoice, paymentAmount, companyId, userId: user.id },
})
} catch {
// Non-blocking
diff --git a/app/api/supplier-invoices/[id]/route.ts b/app/api/supplier-invoices/[id]/route.ts
index 22e54afd..72f01894 100644
--- a/app/api/supplier-invoices/[id]/route.ts
+++ b/app/api/supplier-invoices/[id]/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { validateBody } from '@/lib/api/validate'
import { UpdateSupplierInvoiceSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(
_request: Request,
@@ -16,11 +17,13 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data: invoice, error } = await supabase
.from('supplier_invoices')
.select('*, supplier:suppliers(*), items:supplier_invoice_items(*), payments:supplier_invoice_payments(*)')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (error || !invoice) {
@@ -43,12 +46,14 @@ export async function PUT(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Only allow editing registered invoices
const { data: existing } = await supabase
.from('supplier_invoices')
.select('status')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (!existing) {
@@ -70,7 +75,7 @@ export async function PUT(
.from('supplier_invoices')
.update(body)
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.select()
.single()
@@ -94,12 +99,14 @@ export async function DELETE(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Only allow deleting registered invoices without journal entries
const { data: existing } = await supabase
.from('supplier_invoices')
.select('status, registration_journal_entry_id')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (!existing) {
@@ -120,7 +127,7 @@ export async function DELETE(
.from('supplier_invoices')
.delete()
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
diff --git a/app/api/supplier-invoices/__tests__/route.test.ts b/app/api/supplier-invoices/__tests__/route.test.ts
index 09d93030..a35312ba 100644
--- a/app/api/supplier-invoices/__tests__/route.test.ts
+++ b/app/api/supplier-invoices/__tests__/route.test.ts
@@ -16,6 +16,11 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
const mockFindFiscalPeriod = vi.fn()
vi.mock('@/lib/bookkeeping/engine', () => ({
findFiscalPeriod: (...args: unknown[]) => mockFindFiscalPeriod(...args),
diff --git a/app/api/supplier-invoices/route.ts b/app/api/supplier-invoices/route.ts
index 38648586..bbef5930 100644
--- a/app/api/supplier-invoices/route.ts
+++ b/app/api/supplier-invoices/route.ts
@@ -5,6 +5,7 @@ import { createSupplierInvoiceRegistrationEntry } from '@/lib/bookkeeping/suppli
import { ensureInitialized } from '@/lib/init'
import { validateBody } from '@/lib/api/validate'
import { CreateSupplierInvoiceSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
import type { SupplierInvoice, SupplierInvoiceItem } from '@/types'
ensureInitialized()
@@ -18,13 +19,15 @@ export async function GET(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { searchParams } = new URL(request.url)
const status = searchParams.get('status')
let query = supabase
.from('supplier_invoices')
.select('*, supplier:suppliers(id, name)')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (status && status !== 'all') {
if (status === 'to_pay') {
@@ -52,6 +55,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, CreateSupplierInvoiceSchema)
if (!validation.success) return validation.response
const body = validation.data
@@ -61,7 +66,7 @@ export async function POST(request: Request) {
.from('suppliers')
.select('*')
.eq('id', body.supplier_id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (supplierError || !supplier) {
@@ -111,6 +116,7 @@ export async function POST(request: Request) {
.from('supplier_invoices')
.insert({
user_id: user.id,
+ company_id: companyId,
supplier_id: body.supplier_id,
arrival_number: arrivalNum,
supplier_invoice_number: body.supplier_invoice_number,
@@ -159,7 +165,7 @@ export async function POST(request: Request) {
const { data: settings } = await supabase
.from('company_settings')
.select('accounting_method')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const accountingMethod = settings?.accounting_method || 'accrual'
@@ -169,6 +175,7 @@ export async function POST(request: Request) {
try {
const journalEntry = await createSupplierInvoiceRegistrationEntry(
supabase,
+ companyId,
user.id,
invoice as SupplierInvoice,
items as SupplierInvoiceItem[],
@@ -190,7 +197,7 @@ export async function POST(request: Request) {
try {
await eventBus.emit({
type: 'supplier_invoice.registered',
- payload: { supplierInvoice: invoice as SupplierInvoice, userId: user.id },
+ payload: { supplierInvoice: invoice as SupplierInvoice, companyId, userId: user.id },
})
} catch {
// Non-blocking — event emission failure should not affect the response
diff --git a/app/api/suppliers/[id]/route.ts b/app/api/suppliers/[id]/route.ts
index 3fe18bc2..c4f9ab6c 100644
--- a/app/api/suppliers/[id]/route.ts
+++ b/app/api/suppliers/[id]/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { validateBody } from '@/lib/api/validate'
import { UpdateSupplierSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET(
_request: Request,
@@ -16,12 +17,14 @@ export async function GET(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch supplier
const { data: supplier, error } = await supabase
.from('suppliers')
.select('*')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (error || !supplier) {
@@ -33,7 +36,7 @@ export async function GET(
.from('supplier_invoices')
.select('status, total, remaining_amount, paid_amount')
.eq('supplier_id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
const stats = {
total_outstanding: 0,
@@ -67,6 +70,8 @@ export async function PUT(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const result = await validateBody(request, UpdateSupplierSchema)
if (!result.success) return result.response
const body = result.data
@@ -96,7 +101,7 @@ export async function PUT(
notes: body.notes,
})
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.select()
.single()
@@ -120,12 +125,14 @@ export async function DELETE(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Check for linked invoices
const { count } = await supabase
.from('supplier_invoices')
.select('id', { count: 'exact', head: true })
.eq('supplier_id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (count && count > 0) {
return NextResponse.json(
@@ -138,7 +145,7 @@ export async function DELETE(
.from('suppliers')
.delete()
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
diff --git a/app/api/suppliers/route.ts b/app/api/suppliers/route.ts
index 7fe057ea..6985bbcf 100644
--- a/app/api/suppliers/route.ts
+++ b/app/api/suppliers/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { validateBody } from '@/lib/api/validate'
import { CreateSupplierSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
export async function GET() {
const supabase = await createClient()
@@ -12,10 +13,12 @@ export async function GET() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { data, error } = await supabase
.from('suppliers')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.order('name', { ascending: true })
if (error) {
@@ -34,6 +37,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const result = await validateBody(request, CreateSupplierSchema)
if (!result.success) return result.response
const body = result.data
@@ -42,6 +47,7 @@ export async function POST(request: Request) {
.from('suppliers')
.insert({
user_id: user.id,
+ company_id: companyId,
name: body.name,
supplier_type: body.supplier_type,
email: body.email,
diff --git a/app/api/support/contact/route.ts b/app/api/support/contact/route.ts
index 34a108c1..3c28754b 100644
--- a/app/api/support/contact/route.ts
+++ b/app/api/support/contact/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { getEmailService } from '@/lib/email/service'
import { SUPPORT_RECIPIENT_EMAIL } from '@/lib/support'
+import { requireCompanyId } from '@/lib/company/context'
function escapeHtml(s: string): string {
return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"')
@@ -12,6 +13,8 @@ export async function POST(request: Request) {
const { data: { user } } = await supabase.auth.getUser()
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ await requireCompanyId(supabase, user.id)
+
let body: { subject?: string; message?: string }
try {
body = await request.json()
diff --git a/app/api/tax-deadlines/generate/route.ts b/app/api/tax-deadlines/generate/route.ts
index 07404e50..34ff35e7 100644
--- a/app/api/tax-deadlines/generate/route.ts
+++ b/app/api/tax-deadlines/generate/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { regenerateTaxDeadlinesForUser } from '@/lib/tax/deadline-generator'
+import { requireCompanyId } from '@/lib/company/context'
/**
* POST /api/tax-deadlines/generate
@@ -15,11 +16,13 @@ export async function POST() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch company settings
const { data: settings, error: settingsError } = await supabase
.from('company_settings')
.select('entity_type, moms_period, f_skatt, vat_registered, pays_salaries, fiscal_year_start_month')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (settingsError || !settings) {
@@ -30,7 +33,7 @@ export async function POST() {
}
try {
- const result = await regenerateTaxDeadlinesForUser(supabase, user.id, {
+ const result = await regenerateTaxDeadlinesForUser(supabase, companyId, {
entity_type: settings.entity_type,
moms_period: settings.moms_period,
f_skatt: settings.f_skatt,
diff --git a/app/api/team/accept/route.ts b/app/api/team/accept/route.ts
new file mode 100644
index 00000000..e97016c5
--- /dev/null
+++ b/app/api/team/accept/route.ts
@@ -0,0 +1,133 @@
+import { createClient, createServiceClient } from '@/lib/supabase/server'
+import { NextResponse, type NextRequest } from 'next/server'
+import { hashInviteToken } from '@/lib/auth/invite-tokens'
+
+/**
+ * GET /api/team/accept?token=xxx
+ * Validates an invite token and returns invite info (for the invite page).
+ * Only company invitations are supported — team invitations are disabled.
+ * No auth required — this is a public endpoint.
+ */
+export async function GET(request: NextRequest) {
+ const token = request.nextUrl.searchParams.get('token')
+ if (!token) {
+ return NextResponse.json({ error: 'Token saknas.' }, { status: 400 })
+ }
+
+ const tokenHash = hashInviteToken(token)
+ const serviceClient = createServiceClient()
+
+ const { data: companyInvite } = await serviceClient
+ .from('company_invitations')
+ .select('id, email, status, expires_at, company_id, companies:company_id(name)')
+ .eq('token_hash', tokenHash)
+ .single()
+
+ if (!companyInvite) {
+ return NextResponse.json({ error: 'Inbjudan hittades inte eller är ogiltig.' }, { status: 404 })
+ }
+
+ if (companyInvite.status !== 'pending') {
+ return NextResponse.json({ error: 'Inbjudan har redan använts.' }, { status: 410 })
+ }
+
+ const expired = new Date(companyInvite.expires_at) < new Date()
+
+ const { data: existingUsers } = await serviceClient.auth.admin.listUsers()
+ const alreadyHasAccount = existingUsers?.users?.some(
+ (u) => u.email?.toLowerCase() === companyInvite.email.toLowerCase()
+ ) ?? false
+
+ return NextResponse.json({
+ data: {
+ type: 'company',
+ companyName: (companyInvite.companies as unknown as { name: string })?.name || 'Företag',
+ email: companyInvite.email,
+ expired,
+ alreadyHasAccount,
+ },
+ })
+}
+
+/**
+ * POST /api/team/accept
+ * Accepts a company invite after the user has signed up.
+ * Team invitations are disabled — teams are single-user.
+ */
+export async function POST(request: NextRequest) {
+ 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 token = body.token as string
+ if (!token) {
+ return NextResponse.json({ error: 'Token saknas.' }, { status: 400 })
+ }
+
+ const tokenHash = hashInviteToken(token)
+ const serviceClient = createServiceClient()
+
+ const { data: companyInvite, error: companyLookupError } = await serviceClient
+ .from('company_invitations')
+ .select('id, company_id, email, role, status, expires_at')
+ .eq('token_hash', tokenHash)
+ .single()
+
+ if (companyLookupError) {
+ console.error('[team/accept] company lookup error:', companyLookupError.message)
+ }
+
+ if (!companyInvite || companyInvite.status !== 'pending') {
+ return NextResponse.json({ error: 'Inbjudan är ogiltig.' }, { status: 400 })
+ }
+
+ if (new Date(companyInvite.expires_at) < new Date()) {
+ await serviceClient
+ .from('company_invitations')
+ .update({ status: 'expired' })
+ .eq('id', companyInvite.id)
+ return NextResponse.json({ error: 'Inbjudan har gått ut.' }, { status: 410 })
+ }
+
+ if (user.email?.toLowerCase() !== companyInvite.email.toLowerCase()) {
+ return NextResponse.json({ error: 'E-postadressen matchar inte inbjudan.' }, { status: 403 })
+ }
+
+ // Add user to company
+ const { error: memberError } = await serviceClient
+ .from('company_members')
+ .insert({
+ company_id: companyInvite.company_id,
+ user_id: user.id,
+ role: companyInvite.role,
+ source: 'direct',
+ })
+
+ if (memberError) {
+ if (memberError.code === '23505') {
+ return NextResponse.json({ error: 'Du är redan medlem.' }, { status: 409 })
+ }
+ return NextResponse.json({ error: 'Kunde inte lägga till medlem.' }, { status: 500 })
+ }
+
+ // Set active company
+ await serviceClient
+ .from('user_preferences')
+ .upsert({
+ user_id: user.id,
+ active_company_id: companyInvite.company_id,
+ }, { onConflict: 'user_id' })
+
+ // Mark invite as accepted
+ await serviceClient
+ .from('company_invitations')
+ .update({ status: 'accepted' })
+ .eq('id', companyInvite.id)
+
+ return NextResponse.json({
+ data: { type: 'company', companyId: companyInvite.company_id },
+ })
+}
diff --git a/app/api/team/invite/[id]/route.ts b/app/api/team/invite/[id]/route.ts
new file mode 100644
index 00000000..ff7ac7d7
--- /dev/null
+++ b/app/api/team/invite/[id]/route.ts
@@ -0,0 +1,12 @@
+import { NextResponse } from 'next/server'
+
+/**
+ * DELETE /api/team/invite/[id]
+ * Team-level invitations are disabled.
+ */
+export async function DELETE() {
+ return NextResponse.json(
+ { error: 'Teaminbjudningar är inaktiverade.' },
+ { status: 403 }
+ )
+}
diff --git a/app/api/team/invite/route.ts b/app/api/team/invite/route.ts
new file mode 100644
index 00000000..90b8583b
--- /dev/null
+++ b/app/api/team/invite/route.ts
@@ -0,0 +1,13 @@
+import { NextResponse } from 'next/server'
+
+/**
+ * POST /api/team/invite
+ * Team-level invitations are disabled.
+ * Invitations should be made at the företag (company) level instead.
+ */
+export async function POST() {
+ return NextResponse.json(
+ { error: 'Teaminbjudningar är inaktiverade. Bjud in via enskilda företag.' },
+ { status: 403 }
+ )
+}
diff --git a/app/api/team/members/[id]/route.ts b/app/api/team/members/[id]/route.ts
new file mode 100644
index 00000000..609b73bd
--- /dev/null
+++ b/app/api/team/members/[id]/route.ts
@@ -0,0 +1,12 @@
+import { NextResponse } from 'next/server'
+
+/**
+ * DELETE /api/team/members/[id]
+ * Removing team members is disabled — teams are single-user.
+ */
+export async function DELETE() {
+ return NextResponse.json(
+ { error: 'Team har bara en ägare och kan inte ändras.' },
+ { status: 403 }
+ )
+}
diff --git a/app/api/team/members/route.ts b/app/api/team/members/route.ts
new file mode 100644
index 00000000..fcc07a05
--- /dev/null
+++ b/app/api/team/members/route.ts
@@ -0,0 +1,91 @@
+import { createClient, createServiceClient } from '@/lib/supabase/server'
+import { NextResponse } from 'next/server'
+
+/**
+ * GET /api/team/members
+ * Returns team members (single-user teams — no invitations).
+ */
+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 serviceClient = await createServiceClient()
+
+ // Find the user's team membership
+ const { data: myMembership } = await serviceClient
+ .from('team_members')
+ .select('team_id, role')
+ .eq('user_id', user.id)
+ .limit(1)
+ .single()
+
+ if (!myMembership) {
+ // User is not in any team — check if they own a company (could start a team)
+ const { data: ownedCompany } = await serviceClient
+ .from('company_members')
+ .select('id')
+ .eq('user_id', user.id)
+ .eq('role', 'owner')
+ .limit(1)
+ .maybeSingle()
+
+ return NextResponse.json({
+ data: {
+ members: [],
+ teamName: null,
+ teamId: null,
+ isOwner: false,
+ hasTeam: false,
+ ownsCompany: !!ownedCompany,
+ },
+ })
+ }
+
+ const teamId = myMembership.team_id
+ const isOwner = myMembership.role === 'owner'
+
+ // Fetch team name
+ const { data: team } = await serviceClient
+ .from('teams')
+ .select('name')
+ .eq('id', teamId)
+ .single()
+
+ // Fetch all team members (owner is a real row now)
+ const { data: members, error: membersError } = await serviceClient
+ .from('team_members')
+ .select('id, team_id, user_id, role, joined_at')
+ .eq('team_id', teamId)
+ .order('joined_at', { ascending: true })
+
+ if (membersError) {
+ return NextResponse.json({ error: 'Kunde inte hämta teammedlemmar.' }, { status: 500 })
+ }
+
+ // Fetch emails from profiles
+ const userIds = (members || []).map((m) => m.user_id)
+ const { data: profiles } = await serviceClient
+ .from('profiles')
+ .select('id, email')
+ .in('id', userIds)
+
+ const emailMap = new Map((profiles || []).map((p) => [p.id, p.email]))
+
+ return NextResponse.json({
+ data: {
+ members: (members || []).map((m) => ({
+ id: m.id,
+ user_id: m.user_id,
+ email: emailMap.get(m.user_id) || '',
+ role: m.role,
+ joined_at: m.joined_at,
+ is_current_user: m.user_id === user.id,
+ })),
+ teamName: team?.name || null,
+ teamId,
+ isOwner,
+ hasTeam: true,
+ },
+ })
+}
diff --git a/app/api/transactions/[id]/book/__tests__/route.test.ts b/app/api/transactions/[id]/book/__tests__/route.test.ts
index d99ef199..3d75ea05 100644
--- a/app/api/transactions/[id]/book/__tests__/route.test.ts
+++ b/app/api/transactions/[id]/book/__tests__/route.test.ts
@@ -18,6 +18,10 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
const mockCreateJournalEntry = vi.fn()
vi.mock('@/lib/bookkeeping/engine', () => ({
createJournalEntry: (...args: unknown[]) => mockCreateJournalEntry(...args),
@@ -155,7 +159,7 @@ describe('POST /api/transactions/[id]/book', () => {
expect(body.journal_entry_id).toBe('je-new')
expect(body.data.id).toBe('je-new')
- expect(mockCreateJournalEntry).toHaveBeenCalledWith(expect.anything(), 'user-1', {
+ expect(mockCreateJournalEntry).toHaveBeenCalledWith(expect.anything(), 'company-1', 'user-1', {
fiscal_period_id: VALID_UUID,
entry_date: '2025-01-15',
description: 'Test booking',
diff --git a/app/api/transactions/[id]/book/route.ts b/app/api/transactions/[id]/book/route.ts
index 621a22cb..f2a2a057 100644
--- a/app/api/transactions/[id]/book/route.ts
+++ b/app/api/transactions/[id]/book/route.ts
@@ -5,6 +5,7 @@ import { ensureInitialized } from '@/lib/init'
import { createJournalEntry } from '@/lib/bookkeeping/engine'
import { validateBody } from '@/lib/api/validate'
import { BookTransactionSchema } from '@/lib/api/schemas'
+import { requireCompanyId } from '@/lib/company/context'
import type { Transaction } from '@/types'
ensureInitialized()
@@ -22,6 +23,8 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, BookTransactionSchema)
if (!validation.success) return validation.response
const { fiscal_period_id, entry_date, description, lines } = validation.data
@@ -31,7 +34,7 @@ export async function POST(
.from('transactions')
.select('*')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchError || !transaction) {
@@ -49,7 +52,7 @@ export async function POST(
// Create journal entry via the engine
let journalEntry
try {
- journalEntry = await createJournalEntry(supabase, user.id, {
+ journalEntry = await createJournalEntry(supabase, companyId, user.id, {
fiscal_period_id,
entry_date,
description,
@@ -90,6 +93,7 @@ export async function POST(
account: lines[0]?.account_number || '',
taxCode: '',
userId: user.id,
+ companyId,
},
})
} catch {
diff --git a/app/api/transactions/[id]/categorize/__tests__/route.test.ts b/app/api/transactions/[id]/categorize/__tests__/route.test.ts
index 0f97c3a6..cdff8a3a 100644
--- a/app/api/transactions/[id]/categorize/__tests__/route.test.ts
+++ b/app/api/transactions/[id]/categorize/__tests__/route.test.ts
@@ -17,6 +17,11 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
const mockBuildMappingResultFromCategory = vi.fn()
vi.mock('@/lib/bookkeeping/category-mapping', () => ({
buildMappingResultFromCategory: (...args: unknown[]) =>
@@ -161,7 +166,7 @@ describe('POST /api/transactions/[id]/categorize', () => {
expect(body.category).toBe('expense_software')
expect(mockSaveUserMappingRule).toHaveBeenCalledWith(
expect.anything(),
- 'user-1',
+ 'company-1',
'GitHub',
'6200',
'1930',
diff --git a/app/api/transactions/[id]/categorize/route.ts b/app/api/transactions/[id]/categorize/route.ts
index 99b1fa17..d961eaeb 100644
--- a/app/api/transactions/[id]/categorize/route.ts
+++ b/app/api/transactions/[id]/categorize/route.ts
@@ -7,6 +7,7 @@ import { getTemplateById, buildMappingResultFromTemplate, validateTemplateForEnt
import { createTransactionJournalEntry } from '@/lib/bookkeeping/transaction-entries'
import { saveUserMappingRule } from '@/lib/bookkeeping/mapping-engine'
import { upsertCounterpartyTemplate, buildMappingResultFromCounterpartyTemplate } from '@/lib/bookkeeping/counterparty-templates'
+import { requireCompanyId } from '@/lib/company/context'
import type { CategorizationTemplate } from '@/types'
import { validateBody } from '@/lib/api/validate'
import { CategorizeTransactionSchema } from '@/lib/api/schemas'
@@ -20,6 +21,7 @@ ensureInitialized()
async function ensureFiscalPeriod(
supabase: Awaited>,
userId: string,
+ companyId: string,
date: string,
fiscalYearStartMonth: number = 1
): Promise {
@@ -27,7 +29,7 @@ async function ensureFiscalPeriod(
const { data: existing } = await supabase
.from('fiscal_periods')
.select('id')
- .eq('user_id', userId)
+ .eq('company_id', companyId)
.lte('period_start', date)
.gte('period_end', date)
.eq('is_closed', false)
@@ -68,11 +70,12 @@ async function ensureFiscalPeriod(
.from('fiscal_periods')
.upsert({
user_id: userId,
+ company_id: companyId,
name: periodName,
period_start: periodStart,
period_end: periodEnd,
}, {
- onConflict: 'user_id,period_start,period_end',
+ onConflict: 'company_id,period_start,period_end',
})
if (error) {
@@ -96,6 +99,8 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Parse and validate request body
const validation = await validateBody(request, CategorizeTransactionSchema)
if (!validation.success) return validation.response
@@ -107,7 +112,7 @@ export async function POST(
.from('transactions')
.select('*')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchError || !transaction) {
@@ -149,7 +154,7 @@ export async function POST(
const { data: settings } = await supabase
.from('company_settings')
.select('entity_type, fiscal_year_start_month')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const entityType: EntityType = (settings?.entity_type as EntityType) || 'enskild_firma'
@@ -188,7 +193,7 @@ export async function POST(
.from('categorization_templates')
.select('*')
.eq('id', body.counterparty_template_id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('is_active', true)
.maybeSingle()
@@ -233,7 +238,7 @@ export async function POST(
const { data: accountExists } = await supabase
.from('chart_of_accounts')
.select('account_number, account_class')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('account_number', body.account_override)
.single()
@@ -266,7 +271,7 @@ export async function POST(
}
// Ensure fiscal period exists for the transaction date
- await ensureFiscalPeriod(supabase, user.id, transaction.date, fiscalYearStartMonth)
+ await ensureFiscalPeriod(supabase, user.id, companyId, transaction.date, fiscalYearStartMonth)
// Try to create journal entry
let journalEntryCreated = false
@@ -276,6 +281,7 @@ export async function POST(
try {
const journalEntry = await createTransactionJournalEntry(
supabase,
+ companyId,
user.id,
transaction as Transaction,
mappingResult
@@ -296,7 +302,7 @@ export async function POST(
try {
await saveUserMappingRule(
supabase,
- user.id,
+ companyId,
transaction.merchant_name,
mappingResult.debit_account,
mappingResult.credit_account,
@@ -333,7 +339,7 @@ export async function POST(
.from('document_attachments')
.update({ journal_entry_id: journalEntryId })
.eq('id', receipt.document_id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
}
} catch (linkErr) {
console.error('[categorize] Failed to link receipt document:', linkErr)
@@ -347,7 +353,7 @@ export async function POST(
.from('invoice_inbox_items')
.update({ status: 'confirmed' })
.eq('id', body.inbox_item_id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
// Link inbox item's document to the journal entry
if (journalEntryId) {
@@ -362,7 +368,7 @@ export async function POST(
.from('document_attachments')
.update({ journal_entry_id: journalEntryId })
.eq('id', inboxItem.document_id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
}
}
} catch (inboxErr) {
@@ -395,6 +401,7 @@ export async function POST(
account: mappingResult.debit_account,
taxCode: mappingResult.vat_lines[0]?.account_number || '',
userId: user.id,
+ companyId,
},
})
diff --git a/app/api/transactions/[id]/describe/__tests__/route.test.ts b/app/api/transactions/[id]/describe/__tests__/route.test.ts
index d25f774f..f4427d4d 100644
--- a/app/api/transactions/[id]/describe/__tests__/route.test.ts
+++ b/app/api/transactions/[id]/describe/__tests__/route.test.ts
@@ -37,6 +37,11 @@ vi.mock('@/lib/supabase/server', () => ({
createClient: (...args: unknown[]) => mockCreateClient(...args),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
describe('POST /api/transactions/[id]/describe', () => {
let POST: typeof import('../route').POST
diff --git a/app/api/transactions/[id]/describe/route.ts b/app/api/transactions/[id]/describe/route.ts
index 50993814..2e2c7cf5 100644
--- a/app/api/transactions/[id]/describe/route.ts
+++ b/app/api/transactions/[id]/describe/route.ts
@@ -6,6 +6,7 @@ import { DescribeTransactionSchema } from '@/lib/api/schemas'
import { extensionRegistry } from '@/lib/extensions/registry'
import { findMatchingTemplates, type TemplateMatch } from '@/lib/bookkeeping/booking-templates'
import { findCounterpartyTemplate, buildMappingResultFromCounterpartyTemplate, formatCounterpartyName } from '@/lib/bookkeeping/counterparty-templates'
+import { requireCompanyId } from '@/lib/company/context'
import type { Transaction, EntityType, VatTreatment } from '@/types'
import type { Extension } from '@/lib/extensions/types'
@@ -82,6 +83,8 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, DescribeTransactionSchema)
if (!validation.success) return validation.response
const { description } = validation.data
@@ -91,7 +94,7 @@ export async function POST(
.from('transactions')
.select('*')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchError || !transaction) {
@@ -102,7 +105,7 @@ export async function POST(
const { data: settings } = await supabase
.from('company_settings')
.select('entity_type')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const entityType: EntityType = (settings?.entity_type as EntityType) || 'enskild_firma'
@@ -155,7 +158,7 @@ export async function POST(
const { count } = await supabase
.from('transactions')
.select('id', { count: 'exact', head: true })
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('merchant_name', transaction.merchant_name)
.is('journal_entry_id', null)
.neq('id', id)
diff --git a/app/api/transactions/[id]/match-invoice/__tests__/route.test.ts b/app/api/transactions/[id]/match-invoice/__tests__/route.test.ts
index f9f94343..4b3facef 100644
--- a/app/api/transactions/[id]/match-invoice/__tests__/route.test.ts
+++ b/app/api/transactions/[id]/match-invoice/__tests__/route.test.ts
@@ -40,6 +40,10 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
import { POST } from '../route'
const VALID_UUID = '550e8400-e29b-41d4-a716-446655440000'
@@ -212,6 +216,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
// Verify accrual payment entry was called with paymentAmount
expect(mockCreateInvoicePaymentJournalEntry).toHaveBeenCalledWith(
expect.anything(),
+ 'company-1',
'user-1',
expect.objectContaining({ id: VALID_UUID }),
'2024-06-15',
@@ -270,7 +275,7 @@ describe('POST /api/transactions/[id]/match-invoice', () => {
expect(status).toBe(200)
expect(body.success).toBe(true)
expect(body.journal_entry_id).toBe('je-payment')
- expect(mockReverseEntry).toHaveBeenCalledWith(expect.anything(), 'user-1', 'je-conflict')
+ expect(mockReverseEntry).toHaveBeenCalledWith(expect.anything(), 'company-1', 'user-1', 'je-conflict')
})
it('returns 500 when storno fails — no partial state change', async () => {
diff --git a/app/api/transactions/[id]/match-invoice/route.ts b/app/api/transactions/[id]/match-invoice/route.ts
index 594f2678..4ebd6dcd 100644
--- a/app/api/transactions/[id]/match-invoice/route.ts
+++ b/app/api/transactions/[id]/match-invoice/route.ts
@@ -10,6 +10,7 @@ import { MatchInvoiceSchema } from '@/lib/api/schemas'
import { logMatchEvent } from '@/lib/invoices/match-log'
import { eventBus } from '@/lib/events/bus'
import { ensureInitialized } from '@/lib/init'
+import { requireCompanyId } from '@/lib/company/context'
import type { EntityType, Invoice, Transaction } from '@/types'
ensureInitialized()
@@ -39,6 +40,8 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Parse and validate request body
const validation = await validateBody(request, MatchInvoiceSchema)
if (!validation.success) return validation.response
@@ -49,7 +52,7 @@ export async function POST(
.from('transactions')
.select('*')
.eq('id', transactionId)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchTxError || !transaction) {
@@ -77,7 +80,7 @@ export async function POST(
.from('invoices')
.select('*, customer:customers(*), items:invoice_items(*)')
.eq('id', invoice_id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchInvError || !invoice) {
@@ -97,7 +100,7 @@ export async function POST(
// If storno fails, return 500 immediately — nothing else has been modified.
if (transaction.journal_entry_id) {
try {
- await reverseEntry(supabase, user.id, transaction.journal_entry_id)
+ await reverseEntry(supabase, companyId, user.id, transaction.journal_entry_id)
// Clear the journal_entry_id on the transaction
const { error: clearJeError } = await supabase
@@ -136,7 +139,7 @@ export async function POST(
const { data: settings } = await supabase
.from('company_settings')
.select('accounting_method, entity_type')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const accountingMethod = settings?.accounting_method || 'accrual'
@@ -151,6 +154,7 @@ export async function POST(
// Kontantmetoden, full payment: combined revenue entry with per-line VAT rates
const journalEntry = await createInvoiceCashEntry(
supabase,
+ companyId,
user.id,
invoice as Invoice,
transaction.date,
@@ -168,6 +172,7 @@ export async function POST(
// final payment when createInvoiceCashEntry is called.
const journalEntry = await createInvoicePaymentJournalEntry(
supabase,
+ companyId,
user.id,
invoice as Invoice,
transaction.date,
@@ -180,6 +185,7 @@ export async function POST(
// Faktureringsmetoden: clear receivable (Debit 1930, Credit 1510)
const journalEntry = await createInvoicePaymentJournalEntry(
supabase,
+ companyId,
user.id,
invoice as Invoice,
transaction.date,
@@ -235,6 +241,7 @@ export async function POST(
.from('invoice_payments')
.insert({
user_id: user.id,
+ company_id: companyId,
invoice_id,
payment_date: transaction.date,
amount: paidAmount,
@@ -292,6 +299,7 @@ export async function POST(
invoice: invoice as Invoice,
transaction: transaction as Transaction,
userId: user.id,
+ companyId,
},
})
} catch {
diff --git a/app/api/transactions/[id]/match-supplier-invoice/route.ts b/app/api/transactions/[id]/match-supplier-invoice/route.ts
index 5673f7e7..27be3275 100644
--- a/app/api/transactions/[id]/match-supplier-invoice/route.ts
+++ b/app/api/transactions/[id]/match-supplier-invoice/route.ts
@@ -9,6 +9,7 @@ import { MatchSupplierInvoiceSchema } from '@/lib/api/schemas'
import { logMatchEvent } from '@/lib/invoices/match-log'
import { eventBus } from '@/lib/events/bus'
import { ensureInitialized } from '@/lib/init'
+import { requireCompanyId } from '@/lib/company/context'
import type { SupplierInvoice, SupplierInvoiceItem, Transaction } from '@/types'
ensureInitialized()
@@ -31,6 +32,8 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, MatchSupplierInvoiceSchema)
if (!validation.success) return validation.response
const { supplier_invoice_id } = validation.data
@@ -40,7 +43,7 @@ export async function POST(
.from('transactions')
.select('*')
.eq('id', transactionId)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchTxError || !transaction) {
@@ -67,7 +70,7 @@ export async function POST(
.from('supplier_invoices')
.select('*, supplier:suppliers(*), items:supplier_invoice_items(*)')
.eq('id', supplier_invoice_id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (fetchInvError || !invoice) {
@@ -88,7 +91,7 @@ export async function POST(
const { data: settings } = await supabase
.from('company_settings')
.select('accounting_method')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const accountingMethod = settings?.accounting_method || 'accrual'
@@ -100,6 +103,7 @@ export async function POST(
if (accountingMethod === 'cash') {
const journalEntry = await createSupplierInvoiceCashEntry(
supabase,
+ companyId,
user.id,
invoice as SupplierInvoice,
(invoice.items || []) as SupplierInvoiceItem[],
@@ -110,6 +114,7 @@ export async function POST(
} else {
const journalEntry = await createSupplierInvoicePaymentEntry(
supabase,
+ companyId,
user.id,
invoice as SupplierInvoice,
paymentAmount,
@@ -157,6 +162,7 @@ export async function POST(
.from('supplier_invoice_payments')
.insert({
user_id: user.id,
+ company_id: companyId,
supplier_invoice_id,
payment_date: transaction.date,
amount: paymentAmount,
@@ -205,6 +211,7 @@ export async function POST(
supplierInvoice: invoice as SupplierInvoice,
transaction: transaction as Transaction,
userId: user.id,
+ companyId,
},
})
} catch {
diff --git a/app/api/transactions/[id]/uncategorize/__tests__/route.test.ts b/app/api/transactions/[id]/uncategorize/__tests__/route.test.ts
index cab34204..d40e67f5 100644
--- a/app/api/transactions/[id]/uncategorize/__tests__/route.test.ts
+++ b/app/api/transactions/[id]/uncategorize/__tests__/route.test.ts
@@ -16,6 +16,10 @@ vi.mock('@/lib/init', () => ({
ensureInitialized: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
const mockReverseEntry = vi.fn()
vi.mock('@/lib/bookkeeping/engine', () => ({
reverseEntry: (...args: unknown[]) => mockReverseEntry(...args),
@@ -103,6 +107,6 @@ describe('POST /api/transactions/[id]/uncategorize', () => {
expect(status).toBe(200)
expect(body).toEqual({ success: true })
- expect(mockReverseEntry).toHaveBeenCalledWith(mockSupabase, 'user-1', 'je-1')
+ expect(mockReverseEntry).toHaveBeenCalledWith(mockSupabase, 'company-1', 'user-1', 'je-1')
})
})
diff --git a/app/api/transactions/[id]/uncategorize/route.ts b/app/api/transactions/[id]/uncategorize/route.ts
index 9d84748c..b5d6538e 100644
--- a/app/api/transactions/[id]/uncategorize/route.ts
+++ b/app/api/transactions/[id]/uncategorize/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { reverseEntry } from '@/lib/bookkeeping/engine'
import { ensureInitialized } from '@/lib/init'
+import { requireCompanyId } from '@/lib/company/context'
ensureInitialized()
@@ -15,6 +16,8 @@ export async function POST(
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { id } = await params
// Fetch transaction
@@ -22,7 +25,7 @@ export async function POST(
.from('transactions')
.select('id, journal_entry_id')
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (txError || !transaction) {
@@ -38,7 +41,7 @@ export async function POST(
.from('journal_entries')
.select('id, status')
.eq('id', transaction.journal_entry_id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
if (entryError || !entry) {
@@ -51,7 +54,7 @@ export async function POST(
// Storno reversal (legally compliant — never deletes)
try {
- await reverseEntry(supabase, user.id, transaction.journal_entry_id)
+ await reverseEntry(supabase, companyId, user.id, transaction.journal_entry_id)
} catch (err) {
const message = err instanceof Error ? err.message : 'Reversal failed'
return NextResponse.json({ error: message }, { status: 500 })
@@ -66,7 +69,7 @@ export async function POST(
journal_entry_id: null,
})
.eq('id', id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
if (updateError) {
return NextResponse.json({ error: 'Failed to reset transaction' }, { status: 500 })
diff --git a/app/api/transactions/batch-describe/__tests__/route.test.ts b/app/api/transactions/batch-describe/__tests__/route.test.ts
index efa386cb..c12e8ee3 100644
--- a/app/api/transactions/batch-describe/__tests__/route.test.ts
+++ b/app/api/transactions/batch-describe/__tests__/route.test.ts
@@ -65,6 +65,11 @@ vi.mock('@/lib/supabase/server', () => ({
createClient: (...args: unknown[]) => mockCreateClient(...args),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
describe('POST /api/transactions/batch-describe', () => {
let POST: typeof import('../route').POST
@@ -171,7 +176,7 @@ describe('POST /api/transactions/batch-describe', () => {
// Verify mapping rule was saved with user description
expect(mockSaveUserMappingRule).toHaveBeenCalledWith(
expect.anything(),
- 'user-1',
+ 'company-1',
'Staples',
'6110',
'1930',
diff --git a/app/api/transactions/batch-describe/route.ts b/app/api/transactions/batch-describe/route.ts
index d348296e..ec5ad7ea 100644
--- a/app/api/transactions/batch-describe/route.ts
+++ b/app/api/transactions/batch-describe/route.ts
@@ -7,6 +7,7 @@ import { BatchDescribeSchema } from '@/lib/api/schemas'
import { getTemplateById, buildMappingResultFromTemplate } from '@/lib/bookkeeping/booking-templates'
import { createTransactionJournalEntry } from '@/lib/bookkeeping/transaction-entries'
import { saveUserMappingRule } from '@/lib/bookkeeping/mapping-engine'
+import { requireCompanyId } from '@/lib/company/context'
import type { Transaction, EntityType, TransactionCategory } from '@/types'
ensureInitialized()
@@ -20,6 +21,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const validation = await validateBody(request, BatchDescribeSchema)
if (!validation.success) return validation.response
const { merchant_name, template_id, is_business, user_description } = validation.data
@@ -34,7 +37,7 @@ export async function POST(request: Request) {
const { data: settings } = await supabase
.from('company_settings')
.select('entity_type, fiscal_year_start_month')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const entityType: EntityType = (settings?.entity_type as EntityType) || 'enskild_firma'
@@ -44,7 +47,7 @@ export async function POST(request: Request) {
const { data: transactions, error: fetchError } = await supabase
.from('transactions')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('merchant_name', merchant_name)
.is('journal_entry_id', null)
.order('date', { ascending: true })
@@ -99,16 +102,18 @@ export async function POST(request: Request) {
.from('fiscal_periods')
.upsert({
user_id: user.id,
+ company_id: companyId,
name: periodName,
period_start: periodStart,
period_end: periodEnd,
- }, { onConflict: 'user_id,period_start,period_end' })
+ }, { onConflict: 'company_id,period_start,period_end' })
// Create journal entry
let journalEntryId: string | null = null
try {
const journalEntry = await createTransactionJournalEntry(
supabase,
+ companyId,
user.id,
tx as Transaction,
mappingResult
@@ -137,6 +142,7 @@ export async function POST(request: Request) {
account: mappingResult.debit_account,
taxCode: mappingResult.vat_lines[0]?.account_number || '',
userId: user.id,
+ companyId,
},
})
@@ -154,7 +160,7 @@ export async function POST(request: Request) {
const sampleResult = buildMappingResultFromTemplate(template, sampleTx, entityType)
await saveUserMappingRule(
supabase,
- user.id,
+ companyId,
merchant_name,
sampleResult.debit_account,
sampleResult.credit_account,
diff --git a/app/api/transactions/batch-match-invoices/route.ts b/app/api/transactions/batch-match-invoices/route.ts
index f5e2050f..ce7bbf1c 100644
--- a/app/api/transactions/batch-match-invoices/route.ts
+++ b/app/api/transactions/batch-match-invoices/route.ts
@@ -1,6 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { getBestInvoiceMatch } from '@/lib/invoices/invoice-matching'
+import { requireCompanyId } from '@/lib/company/context'
import type { Transaction } from '@/types'
/**
@@ -16,11 +17,13 @@ export async function POST() {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
// Fetch uncategorized income transactions without a potential match
const { data: transactions, error: txError } = await supabase
.from('transactions')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.is('is_business', null)
.gt('amount', 0)
.is('potential_invoice_id', null)
diff --git a/app/api/transactions/suggest-categories/route.ts b/app/api/transactions/suggest-categories/route.ts
index 0c480b4c..dfe08ed2 100644
--- a/app/api/transactions/suggest-categories/route.ts
+++ b/app/api/transactions/suggest-categories/route.ts
@@ -2,6 +2,7 @@ import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { getSuggestedCategories, mergeAiSuggestions, getSuggestedTemplates, type SuggestedCategory, type SuggestedTemplate } from '@/lib/transactions/category-suggestions'
import { findCounterpartyTemplatesBatch, formatCounterpartyName, toCounterpartyTemplateId } from '@/lib/bookkeeping/counterparty-templates'
+import { requireCompanyId } from '@/lib/company/context'
import type { Transaction, EntityType } from '@/types'
/**
@@ -17,6 +18,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const { transaction_ids } = await request.json()
if (!Array.isArray(transaction_ids) || transaction_ids.length === 0) {
@@ -30,7 +33,7 @@ export async function POST(request: Request) {
const { data: transactions, error: txError } = await supabase
.from('transactions')
.select('*')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.in('id', ids)
if (txError || !transactions) {
@@ -41,7 +44,7 @@ export async function POST(request: Request) {
const { data: mappingRules } = await supabase
.from('mapping_rules')
.select('*')
- .or(`user_id.eq.${user.id},user_id.is.null`)
+ .or(`company_id.eq.${companyId},company_id.is.null`)
.eq('is_active', true)
.order('priority', { ascending: false })
@@ -49,7 +52,7 @@ export async function POST(request: Request) {
const { data: historicalTxns } = await supabase
.from('transactions')
.select('category')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.not('is_business', 'is', null)
.neq('category', 'uncategorized')
.neq('category', 'private')
@@ -67,7 +70,7 @@ export async function POST(request: Request) {
const { data: aiRecords } = await supabase
.from('extension_data')
.select('key, value')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.eq('extension_id', 'ai-categorization')
.in('key', aiKeys)
@@ -90,12 +93,12 @@ export async function POST(request: Request) {
const { data: settings } = await supabase
.from('company_settings')
.select('entity_type')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.single()
const entityType = (settings?.entity_type as EntityType) || undefined
// Batch counterparty template matching (1 DB query, in-memory matching)
- const counterpartyMatches = await findCounterpartyTemplatesBatch(supabase, user.id, transactions as Transaction[])
+ const counterpartyMatches = await findCounterpartyTemplatesBatch(supabase, companyId, transactions as Transaction[])
// Generate initial suggestions for each transaction
const suggestions: Record = {}
@@ -147,7 +150,7 @@ export async function POST(request: Request) {
const { data: matchedInboxItems } = await supabase
.from('invoice_inbox_items')
.select('matched_transaction_id, suggested_template_id, suggested_template_confidence')
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
.in('matched_transaction_id', ids)
.not('suggested_template_id', 'is', null)
diff --git a/app/api/vat/validate/__tests__/route.test.ts b/app/api/vat/validate/__tests__/route.test.ts
index 77bd2d2c..3741713a 100644
--- a/app/api/vat/validate/__tests__/route.test.ts
+++ b/app/api/vat/validate/__tests__/route.test.ts
@@ -6,6 +6,11 @@ vi.mock('@/lib/supabase/server', () => ({
createClient: vi.fn(),
}))
+vi.mock('@/lib/company/context', () => ({
+ requireCompanyId: vi.fn().mockResolvedValue('company-1'),
+ getActiveCompanyId: vi.fn().mockResolvedValue('company-1'),
+}))
+
// Mock VIES client
const mockValidateVatNumber = vi.fn()
vi.mock('@/lib/vat/vies-client', () => ({
diff --git a/app/api/vat/validate/route.ts b/app/api/vat/validate/route.ts
index 5c71d493..82d87a8d 100644
--- a/app/api/vat/validate/route.ts
+++ b/app/api/vat/validate/route.ts
@@ -3,6 +3,7 @@ import { NextResponse } from 'next/server'
import { validateBody } from '@/lib/api/validate'
import { ValidateVatNumberSchema } from '@/lib/api/schemas'
import { validateVatNumber } from '@/lib/vat/vies-client'
+import { requireCompanyId } from '@/lib/company/context'
export async function POST(request: Request) {
const supabase = await createClient()
@@ -13,6 +14,8 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
+ const companyId = await requireCompanyId(supabase, user.id)
+
const result = await validateBody(request, ValidateVatNumberSchema)
if (!result.success) return result.response
const { vat_number, customer_id } = result.data
@@ -29,7 +32,7 @@ export async function POST(request: Request) {
vat_number_validated_at: new Date().toISOString(),
})
.eq('id', customer_id)
- .eq('user_id', user.id)
+ .eq('company_id', companyId)
}
return NextResponse.json(validation)
diff --git a/app/companies/new/page.tsx b/app/companies/new/page.tsx
new file mode 100644
index 00000000..f52861e7
--- /dev/null
+++ b/app/companies/new/page.tsx
@@ -0,0 +1,509 @@
+'use client'
+
+import { useState, useEffect, Suspense } from 'react'
+import { useRouter } from 'next/navigation'
+import Image from 'next/image'
+import Link from 'next/link'
+import * as Sentry from '@sentry/nextjs'
+import { createClient } from '@/lib/supabase/client'
+import { switchCompany } from '@/lib/company/actions'
+import { useToast } from '@/components/ui/use-toast'
+import { Loader2, ArrowLeft } from 'lucide-react'
+import { cn } from '@/lib/utils'
+import { validatePeriodDuration } from '@/lib/bookkeeping/validate-period-duration'
+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'
+
+import Step1EntityType from '@/components/onboarding/Step1EntityType'
+import Step2CompanyDetails from '@/components/onboarding/Step2CompanyDetails'
+import Step3TaxRegistration from '@/components/onboarding/Step3TaxRegistration'
+import Step4VatAccounting from '@/components/onboarding/Step4VatAccounting'
+
+const STEP_INFO = [
+ { title: 'Nytt företag', subtitle: 'Välj företagsform för det nya företaget.', label: 'Företagsform' },
+ { title: 'Företagsuppgifter', subtitle: 'Uppgifterna visas på fakturor och dokument.', label: 'Uppgifter' },
+ { title: 'F-skatt & räkenskapsår', subtitle: 'Skatteregistrering och räkenskapsår.', label: 'Skatt' },
+ { title: 'Moms & bokföring', subtitle: 'Momsregistrering och bokföringsmetod.', label: 'Moms' },
+]
+
+function translatePeriodError(msg: string): string {
+ if (msg.includes('end must be after')) return 'Slutdatumet måste vara efter startdatumet.'
+ if (msg.includes('start must be the 1st')) return 'Startdatumet måste vara den 1:a i en månad.'
+ if (msg.includes('end must be the last day')) return 'Slutdatumet måste vara sista dagen i en månad.'
+ if (msg.includes('exceeds maximum 18 months')) return 'Räkenskapsåret får inte överstiga 18 månader (BFL 3 kap.).'
+ return 'Ogiltigt räkenskapsår. Kontrollera datumen och försök igen.'
+}
+
+export default function NewCompanyPage() {
+ return (
+
+
+
+ }>
+
+
+ )
+}
+
+const LOG = '[new-company]'
+
+function logError(message: string, extra?: Record
) {
+ console.error(LOG, message, extra ?? '')
+ fetch('/api/log', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ message: `new-company: ${message}`, extra }),
+ }).catch(() => {})
+ Sentry.captureMessage(`new-company: ${message}`, {
+ level: 'error',
+ extra: { ...extra, component: 'new-company' },
+ })
+}
+
+function NewCompanyContent() {
+ const router = useRouter()
+ const { toast } = useToast()
+ const supabase = createClient()
+
+ const [isLoading, setIsLoading] = useState(true)
+ const [isSaving, setIsSaving] = useState(false)
+ const [currentStep, setCurrentStep] = useState(1)
+ const [settings, setSettings] = useState>({})
+ const [companyId, setCompanyId] = useState(null)
+ const ticEnabled = ENABLED_EXTENSION_IDS.has('tic')
+ const [ticLookup, setTicLookup] = useState(null)
+
+ const totalSteps = 4
+
+ // Just verify auth on mount
+ useEffect(() => {
+ async function checkAuth() {
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) {
+ router.push('/login')
+ return
+ }
+ setIsLoading(false)
+ }
+ checkAuth()
+ }, [supabase, router])
+
+ const saveSettings = async (updates: Partial, nextStep?: number) => {
+ const targetStep = nextStep ?? currentStep
+ setIsSaving(true)
+
+ try {
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) {
+ router.push('/login')
+ return false
+ }
+
+ const updatedSettings = {
+ ...settings,
+ ...updates,
+ onboarding_step: targetStep,
+ }
+
+ if (!companyId) {
+ logError('save aborted: no companyId', { step: targetStep })
+ return false
+ }
+
+ const {
+ id: _id, user_id: _uid, company_id: _cid, created_at: _ca, updated_at: _ua,
+ is_first_fiscal_year: _ify, first_year_start: _fys, first_year_end: _fye,
+ ...settingsToSave
+ } = updatedSettings as Record
+
+ const { error } = await supabase
+ .from('company_settings')
+ .upsert({ ...settingsToSave, company_id: companyId }, { onConflict: 'company_id' })
+
+ if (error) {
+ logError('save failed', { message: error.message, step: targetStep, code: error.code })
+ toast({ title: 'Fel', description: error.message || 'Kunde inte spara. Försök igen.', variant: 'destructive' })
+ return false
+ }
+
+ setSettings(updatedSettings)
+ return true
+ } catch (err) {
+ const message = err instanceof Error ? err.message : String(err)
+ logError('saveSettings threw', { message, step: targetStep })
+ Sentry.captureException(err)
+ toast({ title: 'Fel', description: 'Ett oväntat fel uppstod. Försök igen.', variant: 'destructive' })
+ return false
+ } finally {
+ setIsSaving(false)
+ }
+ }
+
+ const handleNext = async (stepData: Partial) => {
+ if (currentStep === 1 && stepData.entity_type && stepData.entity_type !== settings.entity_type) {
+ stepData = { ...stepData, org_number: '', company_name: '' }
+ setTicLookup(null)
+ }
+
+ // Step 1: Create the new company
+ let activeCompanyId = companyId
+
+ if (currentStep === 1 && !activeCompanyId) {
+ try {
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) {
+ router.push('/login')
+ return
+ }
+
+ // Atomically create company + owner membership + set active
+ const { data: newCompanyId, error: companyError } = await supabase.rpc('create_company_with_owner', {
+ p_name: 'Nytt företag',
+ p_entity_type: stepData.entity_type,
+ })
+
+ if (companyError || !newCompanyId) {
+ logError('company creation failed', { message: companyError?.message })
+ toast({ title: 'Fel', description: 'Kunde inte skapa företag. Försök igen.', variant: 'destructive' })
+ return
+ }
+
+ activeCompanyId = newCompanyId
+ setCompanyId(activeCompanyId)
+ console.log(LOG, 'created company', activeCompanyId)
+ } catch (err) {
+ logError('company creation threw', { error: String(err) })
+ Sentry.captureException(err)
+ toast({ title: 'Fel', description: 'Kunde inte skapa företag. Försök igen.', variant: 'destructive' })
+ return
+ }
+ }
+
+ if (!activeCompanyId) {
+ logError('handleNext aborted: no companyId', { step: currentStep })
+ return
+ }
+
+ const nextStep = currentStep + 1
+
+ // Direct save for step 1 (React batching: companyId state not yet updated)
+ const needsDirectSave = currentStep === 1 && !companyId
+ const success = needsDirectSave
+ ? await (async () => {
+ setIsSaving(true)
+ try {
+ const updatedSettings = { ...settings, ...stepData, onboarding_step: nextStep }
+ const {
+ id: _id, user_id: _uid, company_id: _cid, created_at: _ca, updated_at: _ua,
+ is_first_fiscal_year: _ify, first_year_start: _fys, first_year_end: _fye,
+ ...settingsToSave
+ } = updatedSettings as Record
+
+ const { error } = await supabase
+ .from('company_settings')
+ .upsert({ ...settingsToSave, company_id: activeCompanyId }, { onConflict: 'company_id' })
+
+ if (error) {
+ logError('save failed', { message: error.message, step: nextStep })
+ toast({ title: 'Fel', description: error.message || 'Kunde inte spara. Försök igen.', variant: 'destructive' })
+ return false
+ }
+
+ setSettings(updatedSettings)
+ return true
+ } catch (err) {
+ logError('saveSettings threw', { message: String(err), step: nextStep })
+ Sentry.captureException(err)
+ return false
+ } finally {
+ setIsSaving(false)
+ }
+ })()
+ : await saveSettings(stepData, nextStep)
+
+ if (!success) return
+
+ // Seed chart of accounts after step 1
+ if (currentStep === 1 && stepData.entity_type) {
+ try {
+ const { error: rpcError } = await supabase.rpc('seed_chart_of_accounts', {
+ p_company_id: activeCompanyId,
+ p_entity_type: stepData.entity_type,
+ })
+ if (rpcError) {
+ logError('COA seeding failed', { entity_type: stepData.entity_type, message: rpcError.message })
+ }
+ } catch (err) {
+ logError('COA seeding threw', { error: String(err) })
+ Sentry.captureException(err)
+ }
+ }
+
+ // Create fiscal period after step 3
+ if (currentStep === 3 && activeCompanyId) {
+ try {
+ const isFirstYear = stepData.is_first_fiscal_year as boolean | undefined
+ const firstYearStart = stepData.first_year_start as string | undefined
+ const firstYearEnd = stepData.first_year_end as string | undefined
+
+ let startStr: string
+ let endStr: string
+ let periodName: string
+
+ if (isFirstYear && firstYearStart && firstYearEnd) {
+ startStr = firstYearStart
+ endStr = firstYearEnd
+ const startYear = new Date(firstYearStart).getFullYear()
+ const endYear = new Date(firstYearEnd).getFullYear()
+ periodName = startYear === endYear
+ ? `Första räkenskapsåret ${startYear}`
+ : `Första räkenskapsåret ${startYear}/${endYear}`
+ } else {
+ let startMonth = stepData.fiscal_year_start_month || settings.fiscal_year_start_month || 1
+ if (settings.entity_type === 'enskild_firma') startMonth = 1
+
+ const currentYear = new Date().getFullYear()
+ startStr = `${currentYear}-${String(startMonth).padStart(2, '0')}-01`
+
+ let endYear: number
+ let endMonth: number
+ if (startMonth === 1) {
+ endYear = currentYear
+ endMonth = 12
+ } else {
+ endYear = currentYear + 1
+ endMonth = startMonth - 1
+ }
+ const lastDay = new Date(endYear, endMonth, 0).getDate()
+ endStr = `${endYear}-${String(endMonth).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`
+
+ periodName = startMonth === 1
+ ? `Räkenskapsår ${currentYear}`
+ : `Räkenskapsår ${currentYear}/${currentYear + 1}`
+ }
+
+ const validationError = validatePeriodDuration(startStr, endStr)
+ if (validationError) {
+ logError('fiscal period validation failed', { validationError, startStr, endStr })
+ toast({
+ title: 'Ogiltigt räkenskapsår',
+ description: translatePeriodError(validationError),
+ variant: 'destructive',
+ })
+ setCurrentStep(3)
+ return
+ }
+
+ // Clean up empty fiscal periods
+ const { data: existingPeriods } = await supabase
+ .from('fiscal_periods')
+ .select('id')
+ .eq('company_id', activeCompanyId)
+
+ if (existingPeriods && existingPeriods.length > 0) {
+ for (const ep of existingPeriods) {
+ const { count } = await supabase
+ .from('journal_entries')
+ .select('id', { count: 'exact', head: true })
+ .eq('fiscal_period_id', ep.id)
+
+ if (count === 0) {
+ await supabase.from('fiscal_periods').delete().eq('id', ep.id)
+ }
+ }
+ }
+
+ const { error: upsertError } = await supabase.from('fiscal_periods').upsert({
+ company_id: activeCompanyId,
+ name: periodName,
+ period_start: startStr,
+ period_end: endStr,
+ }, { onConflict: 'company_id,period_start,period_end' })
+
+ if (upsertError) {
+ logError('fiscal period upsert failed', { message: upsertError.message, startStr, endStr })
+ }
+ } catch (err) {
+ logError('fiscal period creation threw', { error: String(err) })
+ Sentry.captureException(err)
+ }
+ }
+
+ // Final step: mark complete, switch to new company, redirect
+ if (nextStep > totalSteps) {
+ const finalSuccess = await saveSettings({ onboarding_complete: true }, totalSteps)
+ if (!finalSuccess) {
+ logError('failed to set onboarding_complete')
+ return
+ }
+
+ // Update company name from settings
+ if (settings.company_name || stepData.company_name) {
+ await supabase
+ .from('companies')
+ .update({ name: settings.company_name || stepData.company_name })
+ .eq('id', activeCompanyId)
+ }
+
+ // Switch active company to the new one
+ await switchCompany(activeCompanyId)
+
+ toast({
+ title: 'Företag skapat!',
+ description: 'Du har nu bytt till det nya företaget.',
+ })
+ router.push('/')
+ } else {
+ setCurrentStep(nextStep)
+ }
+ }
+
+ const handleBack = () => {
+ if (currentStep > 1) {
+ setCurrentStep(currentStep - 1)
+ }
+ }
+
+ if (isLoading) {
+ return (
+
+
+
+ )
+ }
+
+ const stepInfo = STEP_INFO[currentStep - 1]
+
+ return (
+