This commit is contained in:
Jakob Wennberg
2026-02-19 14:32:54 +01:00
parent cdf1dcc4c8
commit e8743e6e03
104 changed files with 4551 additions and 4172 deletions
@@ -1,7 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateChatResponse } from '@/lib/ai/chatbot/chain'
import { CHATBOT_CONFIG } from '@/lib/ai/chatbot/config'
import { generateChatResponse } from '@/extensions/ai-chat/chatbot/chain'
import { CHATBOT_CONFIG } from '@/extensions/ai-chat/chatbot/config'
import type { ChatMessage, ChatRequest } from '@/types/chat'
// Simple in-memory rate limiting (per user)
@@ -1,6 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { streamChatResponse } from '@/lib/ai/chatbot/chain'
import { CHATBOT_CONFIG } from '@/lib/ai/chatbot/config'
import { streamChatResponse } from '@/extensions/ai-chat/chatbot/chain'
import { CHATBOT_CONFIG } from '@/extensions/ai-chat/chatbot/config'
import type { ChatMessage, ChatRequest, SourceReference } from '@/types/chat'
// Simple in-memory rate limiting (per user)
@@ -1,14 +1,14 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { generateNEDeclaration } from '@/lib/reports/ne-declaration'
import { generateNEDeclaration } from '@/extensions/ne-bilaga/ne-engine'
import {
generateSRUFile,
sruFileToString,
getSRUFilename,
} from '@/lib/reports/sru-generator'
} from '@/extensions/sru-export/lib/sru-generator'
/**
* GET /api/reports/ne-declaration
* GET /api/extensions/ne-bilaga
*
* Generate NE declaration (NE-bilaga) for enskild firma.
*
@@ -1,18 +1,22 @@
import { createClient } from '@supabase/supabase-js'
import { NextResponse } from 'next/server'
import { loadExtensions } from '@/lib/extensions/loader'
import {
sendTaxDeadlineNotifications,
sendInvoiceNotifications,
} from '@/lib/push/notification-scheduler'
} from '@/extensions/push-notifications/notification-scheduler'
/**
* GET /api/push/cron
* GET /api/extensions/push-notifications/cron
* Daily cron job to send push notifications
* Runs at 09:00 every day
*
* Vercel Cron: "0 9 * * *"
*/
export async function GET(request: Request) {
// Ensure extensions are loaded so event handlers are registered
loadExtensions()
// Verify cron secret for security
const authHeader = request.headers.get('authorization')
const cronSecret = process.env.CRON_SECRET
@@ -0,0 +1,61 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { getSettings, saveSettings } from '@/extensions/push-notifications'
/**
* GET /api/extensions/push-notifications/settings
* Get the current user's push-notification extension settings
*/
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 settings = await getSettings(user.id)
return NextResponse.json({ data: settings })
}
/**
* PATCH /api/extensions/push-notifications/settings
* Update the current user's push-notification extension settings
*/
export async function PATCH(request: Request) {
const supabase = await createClient()
const {
data: { user },
} = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const body = await request.json()
const allowedKeys = [
'periodLockedEnabled',
'periodYearClosedEnabled',
'invoiceSentEnabled',
'receiptExtractedEnabled',
'receiptMatchedEnabled',
]
const filtered: Record<string, unknown> = {}
for (const key of allowedKeys) {
if (key in body) {
filtered[key] = body[key]
}
}
if (Object.keys(filtered).length === 0) {
return NextResponse.json({ error: 'No valid settings provided' }, { status: 400 })
}
const settings = await saveSettings(user.id, filtered)
return NextResponse.json({ data: settings })
}
@@ -1,9 +1,9 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { getVapidPublicKey } from '@/lib/push/web-push'
import { getVapidPublicKey } from '@/extensions/push-notifications/notification-sender'
/**
* GET /api/push/subscribe
* GET /api/extensions/push-notifications/subscribe
* Get the VAPID public key for client-side subscription
*/
export async function GET() {
@@ -20,7 +20,7 @@ export async function GET() {
}
/**
* POST /api/push/subscribe
* POST /api/extensions/push-notifications/subscribe
* Save a new push subscription
*/
export async function POST(request: Request) {
@@ -96,7 +96,7 @@ export async function POST(request: Request) {
}
/**
* DELETE /api/push/subscribe
* DELETE /api/extensions/push-notifications/subscribe
* Remove a push subscription
*/
export async function DELETE(request: Request) {
@@ -1,6 +1,6 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { findTransactionMatches } from '@/lib/receipts/receipt-matcher'
import { findTransactionMatches } from '@/extensions/receipt-ocr/lib/receipt-matcher'
import { eventBus } from '@/lib/events/bus'
import { ensureInitialized } from '@/lib/init'
import type { Receipt, Transaction } from '@/types'
@@ -1,7 +1,7 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { analyzeReceipt } from '@/lib/receipts/receipt-analyzer'
import { processLineItems } from '@/lib/receipts/receipt-categorizer'
import { analyzeReceipt } from '@/extensions/receipt-ocr/lib/receipt-analyzer'
import { processLineItems } from '@/extensions/receipt-ocr/lib/receipt-categorizer'
import { eventBus } from '@/lib/events/bus'
import { ensureInitialized } from '@/lib/init'
@@ -0,0 +1,29 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { getSRUCoverage } from '@/extensions/sru-export/sru-engine'
/**
* GET /api/extensions/sru-export/coverage
*
* Returns SRU code coverage stats: how many accounts have SRU codes vs total,
* and a list of accounts missing codes.
*/
export async function GET() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
try {
const coverage = await getSRUCoverage(user.id)
return NextResponse.json({ data: coverage })
} catch (err) {
console.error('Error fetching SRU coverage:', err)
return NextResponse.json(
{ error: err instanceof Error ? err.message : 'Failed to fetch SRU coverage' },
{ status: 500 }
)
}
}
+150
View File
@@ -0,0 +1,150 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { aggregateBalancesBySRU } from '@/extensions/sru-export/sru-engine'
import {
generateGenericSRU,
getGenericSRUFilename,
sruFileToString,
validateSRUFile,
SRU_CODE_DESCRIPTIONS,
} from '@/extensions/sru-export/sru-generator'
import type { SRUFormType } from '@/extensions/sru-export/sru-generator'
import type { EntityType } from '@/types'
/**
* GET /api/extensions/sru-export
*
* Generate SRU export from chart_of_accounts sru_code mappings.
*
* Query parameters:
* - period_id: Fiscal period ID (required)
* - format: 'json' (default) or 'sru' for downloadable file
*/
export async function GET(request: Request) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
const format = searchParams.get('format') || 'json'
if (!periodId) {
return NextResponse.json(
{ error: 'period_id is required' },
{ status: 400 }
)
}
try {
// Fetch company settings to determine entity type
const { data: settings } = await supabase
.from('company_settings')
.select('entity_type, company_name, org_number')
.eq('user_id', user.id)
.single()
if (!settings) {
return NextResponse.json(
{ error: 'Company settings not found. Complete onboarding first.' },
{ status: 400 }
)
}
// Determine form type from entity type
const formType: SRUFormType = settings.entity_type === 'aktiebolag' ? 'INK2' : 'NE'
// Fetch fiscal period for date range
const { data: period } = await supabase
.from('fiscal_periods')
.select('*')
.eq('id', periodId)
.eq('user_id', user.id)
.single()
if (!period) {
return NextResponse.json(
{ error: 'Fiscal period not found' },
{ status: 404 }
)
}
// Aggregate balances by SRU code
const sruBalances = await aggregateBalancesBySRU(user.id, periodId)
if (format === 'sru') {
// Generate and return SRU file
const sruFile = generateGenericSRU({
formType,
orgNumber: settings.org_number,
companyName: settings.company_name || 'Okänt företag',
fiscalYearStart: period.period_start,
fiscalYearEnd: period.period_end,
sruBalances,
})
const validation = validateSRUFile(sruFile)
if (!validation.isValid) {
return NextResponse.json(
{ error: 'SRU validation failed', details: validation.errors },
{ status: 500 }
)
}
const sruContent = sruFileToString(sruFile)
const filename = getGenericSRUFilename(formType, settings.org_number, period.period_start)
return new NextResponse(sruContent, {
status: 200,
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Disposition': `attachment; filename="${filename}"`,
},
})
}
// Default: return JSON preview
const balancesArray = Array.from(sruBalances.values())
.sort((a, b) => a.sruCode.localeCompare(b.sruCode))
.map((b) => ({
sruCode: b.sruCode,
description: SRU_CODE_DESCRIPTIONS[b.sruCode] || `SRU ${b.sruCode}`,
amount: b.amount,
accounts: b.accounts,
}))
const warnings: string[] = []
if (!period.is_closed) {
warnings.push('Räkenskapsåret är inte stängt. Siffrorna kan ändras.')
}
if (balancesArray.length === 0) {
warnings.push('Inga konton med SRU-koder har bokförda poster i denna period.')
}
return NextResponse.json({
data: {
formType,
entityType: settings.entity_type as EntityType,
companyName: settings.company_name,
orgNumber: settings.org_number,
fiscalYear: {
id: period.id,
name: period.name,
start: period.period_start,
end: period.period_end,
},
balances: balancesArray,
warnings,
},
})
} catch (err) {
console.error('Error generating SRU export:', err)
return NextResponse.json(
{ error: err instanceof Error ? err.message : 'Failed to generate SRU export' },
{ status: 500 }
)
}
}