feat: multi-tenant company refactor (GNU-19) (#153)
* feat: multi-tenant company refactor (GNU-19) Introduce companies table, company_members, and user_preferences to support multiple companies per user. All data scoping changes from user_id to company_id across the entire codebase. Key changes: - Database migration: new tables, company_id on 40+ tables, backfill, RLS rewrite from user_id to company-member-based, updated RPCs - Types: Company, CompanyMember, CompanyRole, UserPreferences types; company_id added to all entity interfaces; companyId on all events - Engine: all 7 core functions take companyId; storno, period, year-end services updated; 16 report generators updated - Middleware: company context resolution (cookie → prefs → first company) - API routes: ~120 routes updated with requireCompanyId() - Frontend: CompanyProvider context, layout/dashboard/onboarding updated - Extensions: context factory, 9 extensions, all lib files updated - Tests: 1880 tests passing, all helpers updated with company_id defaults Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add database migrations for multi-tenant company and team system (GNU-19) Adds company_invitations, company creation RPC, team_members, account deletion RPC, and teams table refactor migrations. Updates base multi-tenant migration with cascading FKs and onboarding_step column. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add team types and update core infrastructure for multi-tenancy (GNU-19) Adds TeamRole, MemberSource, and Team types. Refactors Supabase service client to be stateless, updates middleware for team-aware routing, extends CompanyContext with team/role fields, and updates extension service types to accept companyId. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: thread company_id through business logic functions (GNU-19) Replaces user_id scoping with company_id across all lib modules: bookkeeping, documents, transactions, invoices, reconciliation, tax, deadlines, and import. Updates corresponding tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: thread company_id through API routes and extensions (GNU-19) Updates all existing API routes to extract and pass companyId. Updates enable-banking and arcim-migration extensions for company-scoped transaction ingestion and sync. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add company and team management API routes (GNU-19) Adds CRUD endpoints for company members, company invitations, team members, and team invitations. Includes invite token utilities, email templates, and company switch server action. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add team/company UI components, pages, and dashboard updates (GNU-19) Adds CompanySwitcher, ConsultantEmptyState, Step0RoleChoice, company members and team management panels. Updates dashboard layout for team-aware routing, onboarding for multi-step role choice, and auth callback for team invite acceptance. Ignores supabase/.branches/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add null guards for company in import page (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move appUrl declaration to outer scope in invite route (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add optional chaining for company.name in members section (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add optional chaining for second company.name in members section (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add null guards for company in extension components (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: pass companyId to executeSIEImport in arcim-migration extension (GNU-19) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update tests to use companyId instead of userId and improve type handling --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
1756af6c5e
commit
0dd1f5ebc1
@@ -1,4 +1,4 @@
|
||||
import { createClient, createServiceClient } from '@/lib/supabase/server'
|
||||
import { createClient } from '@/lib/supabase/server'
|
||||
import { NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
|
||||
@@ -31,9 +31,11 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
try {
|
||||
// Delete the auth user via service role — all data cascades via ON DELETE CASCADE
|
||||
const serviceClient = await createServiceClient()
|
||||
const { error } = await serviceClient.auth.admin.deleteUser(user.id)
|
||||
// RPC disables protective triggers, deletes from auth.users (CASCADE
|
||||
// cleans up all public tables), then re-enables triggers — all in one tx.
|
||||
const { error } = await supabase.rpc('delete_user_account', {
|
||||
target_user_id: user.id,
|
||||
})
|
||||
|
||||
if (error) {
|
||||
console.error('Failed to delete user:', error)
|
||||
|
||||
@@ -15,6 +15,11 @@ vi.mock('@/lib/extensions/ai-consent', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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<string, boolean> = {}
|
||||
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 } })
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
|
||||
@@ -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]),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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 } })
|
||||
}
|
||||
@@ -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 } })
|
||||
}
|
||||
@@ -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 }) },
|
||||
})
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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<CreateDeadlineInput> = 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 })
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, string>()
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<ReturnType<typeof createClient>>,
|
||||
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 },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<ReturnType<typeof createClient>>,
|
||||
userId: string,
|
||||
companyId: string,
|
||||
date: string,
|
||||
fiscalYearStartMonth: number = 1
|
||||
): Promise<boolean> {
|
||||
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<ReturnType<typeof createClient>>,
|
||||
userId: string,
|
||||
companyId: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<{ data?: Record<string, unknown>; 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<ReturnType<typeof createClient>>,
|
||||
userId: string,
|
||||
companyId: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<{ data?: Record<string, unknown>; 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<ReturnType<typeof createClient>>,
|
||||
userId: string,
|
||||
companyId: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<{ data?: Record<string, unknown>; 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<ReturnType<typeof createClient>>,
|
||||
userId: string,
|
||||
companyId: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<{ data?: Record<string, unknown>; 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<ReturnType<typeof createClient>>,
|
||||
userId: string,
|
||||
companyId: string,
|
||||
params: Record<string, unknown>,
|
||||
userEmail?: string
|
||||
): Promise<{ data?: Record<string, unknown>; 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<ReturnType<typeof createClient>>,
|
||||
userId: string,
|
||||
companyId: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<{ data?: Record<string, unknown>; 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<ReturnType<typeof createClient>>,
|
||||
userId: string,
|
||||
companyId: string,
|
||||
params: Record<string, unknown>
|
||||
): Promise<{ data?: Record<string, unknown>; 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 })
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
])
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user