feat(invoices): implement öresavrundning logic and next invoice numbe… (#429)

* feat(invoices): implement öresavrundning logic and next invoice number preview

- Added `getDisplayTotal` utility to handle rounding for SEK invoices based on company settings.
- Updated `InvoicesPage` to utilize the new rounding logic when displaying totals.
- Introduced `peek_next_invoice_number` function to allow previewing the next invoice number without consuming the sequence.
- Modified invoice number generation to remove the year prefix and prevent truncation of numbers exceeding three digits.
- Enhanced tests for invoice number generation and rounding functionality to ensure correctness.
- Updated PDF template to reflect new rounding logic for totals and display appropriate values.
- Adjusted company switcher to hide options in sandbox mode.
- Improved error handling and logging in sandbox seeding process.

* fix(skatteverket): remove unused scope labels from SCOPE_LABELS and DEFAULT_SCOPES
This commit is contained in:
Mattsson
2026-05-10 14:21:26 +02:00
committed by GitHub
parent c110d3ef99
commit 0ee5219b6c
19 changed files with 733 additions and 106 deletions
+38
View File
@@ -0,0 +1,38 @@
import { NextResponse } from 'next/server'
import { withRouteContext } from '@/lib/api/with-route-context'
import { errorResponse } from '@/lib/errors/get-structured-error'
export const GET = withRouteContext(
'invoice.peek_next_number',
async (request, ctx) => {
const { supabase, companyId, log, requestId } = ctx
const url = new URL(request.url)
const documentType = url.searchParams.get('document_type') ?? 'invoice'
if (!['invoice', 'proforma', 'delivery_note'].includes(documentType)) {
return NextResponse.json(
{ error: 'invalid document_type', requestId },
{ status: 400 },
)
}
// delivery_note has its own sequence (generate_delivery_note_number); the
// peek RPC only covers the invoice/proforma F-series counter, so for
// delivery notes we return null and let the form skip the preview.
if (documentType === 'delivery_note') {
return NextResponse.json({ data: { preview: null } })
}
const { data, error } = await supabase.rpc('peek_next_invoice_number', {
p_company_id: companyId,
p_document_type: documentType,
})
if (error) {
log.error('peek_next_invoice_number failed', error)
return errorResponse(error, log, { requestId })
}
return NextResponse.json({ data: { preview: data ?? null } })
},
)
+30 -3
View File
@@ -1,6 +1,9 @@
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
import { requireCompanyId } from '@/lib/company/context'
import { getActiveCompanyId } from '@/lib/company/context'
import { createLogger } from '@/lib/logger'
const log = createLogger('sandbox:seed')
/**
* POST /api/sandbox/seed
@@ -19,7 +22,30 @@ export async function POST() {
return NextResponse.json({ error: 'Sandbox is only available for anonymous users' }, { status: 403 })
}
const companyId = await requireCompanyId(supabase, user.id)
// Anonymous users start with no company. Create one before seeding.
// If a previous seed attempt already created a company for this user, reuse it
// (idempotency).
let companyId = await getActiveCompanyId(supabase, user.id)
if (!companyId) {
const { data: newCompanyId, error: companyError } = await supabase.rpc(
'create_company_with_owner',
{
p_name: 'Sandlådan Konsult',
p_entity_type: 'enskild_firma',
}
)
if (companyError || !newCompanyId) {
log.error('failed to create sandbox company', { error: companyError, userId: user.id })
return NextResponse.json(
{ error: 'Failed to create sandbox company' },
{ status: 500 }
)
}
companyId = newCompanyId as string
}
// Idempotency: if already seeded, return early
const { data: existing } = await supabase
@@ -523,7 +549,8 @@ export async function POST() {
if (dlError) throw dlError
return NextResponse.json({ seeded: true })
} catch {
} catch (err) {
log.error('failed to seed sandbox data', { error: err, userId: user.id, companyId })
return NextResponse.json(
{ error: 'Failed to seed sandbox data' },
{ status: 500 }