Files
accounted/app/api/bookkeeping/journal-entries/route.ts
T
Mattssonandgreptile-apps[bot] 64cd6a0989 Fix/footer UI (#296)
* feat: enhance journal entry handling with follow-up entries and related RPC

* fix: improve validation for journal entry lines to ensure proper submission criteria

* feat: add commit_method and rubric_version columns to journal_entries for enhanced tracking

* fix: ensure conditional addition of commit_method and rubric_version columns in journal_entries

* Update supabase/migrations/20260421120000_journal_entries_with_related_rpc.sql

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-21 12:55:43 +02:00

132 lines
4.2 KiB
TypeScript

import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
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'
import { requireWritePermission } from '@/lib/auth/require-write'
ensureInitialized()
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 companyId = await requireCompanyId(supabase, user.id)
const { searchParams } = new URL(request.url)
const periodId = searchParams.get('period_id')
const status = searchParams.get('status')
const limit = parseInt(searchParams.get('limit') || '50')
const offset = parseInt(searchParams.get('offset') || '0')
const dateFrom = searchParams.get('date_from')
const dateTo = searchParams.get('date_to')
const sortDate = searchParams.get('sort_date') // 'asc' | 'desc'
// Default on: when a fiscal period is selected, include follow-up entries
// booked in later periods whose source aggregate (invoice, supplier invoice)
// is dated inside the selected period. Pass include_related=false to
// restore strict fiscal_period_id filtering.
const includeRelated = searchParams.get('include_related') !== 'false'
const dateAscending = sortDate === 'asc'
const sortDateParam = sortDate === 'asc' || sortDate === 'desc' ? sortDate : 'desc'
if (periodId && includeRelated) {
const { data, error } = await supabase.rpc('list_fiscal_period_entries_with_related', {
p_company_id: companyId,
p_period_id: periodId,
p_include_related: true,
p_status: status,
p_date_from: dateFrom,
p_date_to: dateTo,
p_sort_date: sortDateParam,
p_limit: limit,
p_offset: offset,
})
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
const rows = data ?? []
const entries = rows.map((r: { entry: unknown }) => r.entry)
const count = rows.length > 0 ? Number((rows[0] as { total_count: number | string }).total_count) : 0
return NextResponse.json({ data: entries, count })
}
let query = supabase
.from('journal_entries')
.select('*, lines:journal_entry_lines(*)', { count: 'exact' })
.eq('company_id', companyId)
if (sortDate === 'asc' || sortDate === 'desc') {
query = query
.order('entry_date', { ascending: dateAscending })
.order('voucher_number', { ascending: dateAscending })
} else {
query = query
.order('voucher_series', { ascending: true })
.order('voucher_number', { ascending: true })
}
query = query.range(offset, offset + limit - 1)
if (periodId) {
query = query.eq('fiscal_period_id', periodId)
}
if (status) {
query = query.eq('status', status)
}
if (dateFrom) {
query = query.gte('entry_date', dateFrom)
}
if (dateTo) {
query = query.lte('entry_date', dateTo)
}
const { data, error, count } = await query
if (error) {
return NextResponse.json({ error: error.message }, { status: 500 })
}
return NextResponse.json({ data, count })
}
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 writeCheck = await requireWritePermission(supabase, user.id)
if (!writeCheck.ok) return writeCheck.response
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, companyId, user.id, body)
return NextResponse.json({ data: entry })
} catch (err) {
return NextResponse.json(
{ error: err instanceof Error ? err.message : 'Failed to create journal entry' },
{ status: 400 }
)
}
}