Add/bokslut (#718)
* feat(arcim-migration): Briox provider with SIE-over-API import - Briox auth via account ID + application token (no app-level credentials); both tokens rotate on refresh and are persisted - New sie-fetcher pulls the general ledger as SIE through the provider API for Fortnox, Briox and Bjorn Lunden - Wizard stops on a failed SIE import and surfaces the real errors instead of proceeding to the misleading migrate-guard message - PROVIDER_SIE_ONLY_FORTNOX renamed to PROVIDER_SIE_NOT_SUPPORTED; new PROVIDER_TOKEN_INVALID for rejected provider credentials Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): per-line accruals (periodisering) on invoices and supplier invoices Defer revenue/costs per invoice line to 29xx/17xx interim accounts with automatic monthly dissolution (nightly cron + catch-up at registration), schedule cancellation on credit, year-end auto-detect exclusion for already-scheduled invoices, invoice-inbox service-period extraction for prefill, and an MCP tool to list schedules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bokslut): iXBRL arsredovisning generation and Bolagsverket digital filing Generate the annual report as iXBRL from a generated taxonomy registry (K2 element lists, taxonomy:generate/check scripts + CI guard), expose it via the fiscal-period API, and add the bolagsverket extension for digital submission to eget utrymme with webhook-driven status tracking (submissions table + pg tests, lifecycle events, year-end wizard UI). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(mcp): raise origin-guard test timeout to 20s The dynamic import pulls in the full server module; the parse alone flirts with the 5s default under full-suite parallel load. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add new scripts and documentation for K2 AB taxonomy generation and validation - Introduced `generate-taxonomy-registry.ts` to automate the generation of the iXBRL taxonomy concept registry from official element lists and tuple models. - Added `validate-ixbrl.mjs` for validating generated iXBRL reports against the official taxonomy package using Arelle. - Included new documentation files: - `k2-ab-arsredovisning-elementlista-2024-09-12_rev20250312_sv.xlsx` - `tuple-innehallsmodell-arsredovisning-k2-2024-09-12.xlsx` - `taxonomi-paket-2024-09-12_rev20250312.zip` * Add tests for bookkeeping accruals dissolution and supplier invoices - Implement tests for the POST /api/bookkeeping/accruals/[id]/dissolve route, covering success and error scenarios. - Add tests for the DELETE /api/supplier-invoices/[id] route, including authentication checks and validation of invoice deletion conditions. - Introduce tests for the Arcim migration provider client, ensuring token handling and error classification. - Create tests for the Bolagsverket extension, validating submission role enforcement and environment settings. - Add Zod schemas for Bolagsverket response payloads to ensure proper validation. - Implement tests for MCP server's list accrual schedules, confirming registration and scope mapping. - Add consistency tests for IXBRL document generation, ensuring duplicate facts and XML escaping are handled correctly. - Introduce typed domain errors for accrual schedules to improve error handling in the service. - Add tests for resolving consent with Briox token refresh concurrency, ensuring proper token management and error handling. * fix(tests): update payload size guard comments to reflect recent changes in tool descriptions and ceiling adjustments * fix(gitattributes): mark generated JSON files in bokslut taxonomy as linguist-generated * feat(migrations): add backfill for invoices.journal_entry_id and fallback for next_voucher_number user_id * feat(bokslut): enhance compliance and financial processing features with new submission details and security measures --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8e8b63a200
commit
db8983ba9e
+109
-1
@@ -1,6 +1,7 @@
|
||||
import { z } from 'zod'
|
||||
import { normaliseSwish, isValidSwish } from '@/lib/payments/swish'
|
||||
import { isSaneDateString } from '@/lib/utils'
|
||||
import { countCalendarMonths } from '@/lib/bookkeeping/accruals/compute'
|
||||
|
||||
// ============================================================
|
||||
// Shared primitives
|
||||
@@ -39,6 +40,61 @@ const vatRatePercent = z.union([z.literal(0), z.literal(6), z.literal(12), z.lit
|
||||
/** Time string (HH:MM or HH:MM:SS) */
|
||||
const timeString = z.string().regex(/^\d{2}:\d{2}(:\d{2})?$/, 'Expected HH:MM or HH:MM:SS time format')
|
||||
|
||||
/** Periodisering: interim accounts. Förutbetalda kostnader live on 17xx. */
|
||||
const prepaidExpenseAccount = z
|
||||
.string()
|
||||
.regex(/^17\d{2}$/, 'Balanskonto för periodiserad kostnad måste vara ett 17xx-konto')
|
||||
|
||||
/** Periodisering: förutbetalda intäkter live on 29xx. */
|
||||
const deferredRevenueAccount = z
|
||||
.string()
|
||||
.regex(/^29\d{2}$/, 'Balanskonto för periodiserad intäkt måste vara ett 29xx-konto')
|
||||
|
||||
/**
|
||||
* Shared periodisering period rules for invoice line items: both dates or
|
||||
* neither, end after start, and a 2–120 calendar month span. The amount-side
|
||||
* rules differ per item shape and stay in each schema's superRefine.
|
||||
*/
|
||||
function validateAccrualPeriod(
|
||||
item: { accrual_period_start?: string | null; accrual_period_end?: string | null },
|
||||
ctx: z.RefinementCtx,
|
||||
): void {
|
||||
const start = item.accrual_period_start
|
||||
const end = item.accrual_period_end
|
||||
if (!start && !end) return
|
||||
if (!start || !end) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['accrual_period_start'],
|
||||
message: 'Ange både periodens start och slut för periodisering',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (end < start) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['accrual_period_end'],
|
||||
message: 'Periodens slut måste vara efter dess start',
|
||||
})
|
||||
return
|
||||
}
|
||||
const months = countCalendarMonths(start, end)
|
||||
if (months < 2) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['accrual_period_end'],
|
||||
message: 'Periodisering kräver minst 2 kalendermånader',
|
||||
})
|
||||
}
|
||||
if (months > 120) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['accrual_period_end'],
|
||||
message: 'Periodisering kan omfatta högst 120 månader',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Enum schemas (matching types/index.ts)
|
||||
// ============================================================
|
||||
@@ -145,6 +201,7 @@ export const JournalEntrySourceTypeSchema = z.enum([
|
||||
'supplier_credit_note',
|
||||
'currency_revaluation',
|
||||
'reminder_fee',
|
||||
'accrual',
|
||||
])
|
||||
|
||||
/** Query params for GET /api/bookkeeping/voucher-sequences/next. */
|
||||
@@ -225,8 +282,39 @@ export const CreateInvoiceItemSchema = z
|
||||
work_type: z.string().max(64).nullable().optional(),
|
||||
housing_designation: z.string().max(128).nullable().optional(),
|
||||
apartment_number: z.string().max(32).nullable().optional(),
|
||||
// Periodisering (förutbetald intäkt): defer the line's net revenue over
|
||||
// the service period. The revenue entry credits the 29xx interim account
|
||||
// instead of the revenue account; output VAT is never deferred.
|
||||
accrual_period_start: isoDate.nullable().optional(),
|
||||
accrual_period_end: isoDate.nullable().optional(),
|
||||
accrual_balance_account: deferredRevenueAccount.nullable().optional(),
|
||||
})
|
||||
.superRefine((item, ctx) => {
|
||||
validateAccrualPeriod(item, ctx)
|
||||
const hasAccrual = Boolean(item.accrual_period_start || item.accrual_period_end)
|
||||
if (hasAccrual) {
|
||||
if (item.line_type === 'text') {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['accrual_period_start'],
|
||||
message: 'Textrader kan inte periodiseras',
|
||||
})
|
||||
}
|
||||
if (item.deduction_type) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['accrual_period_start'],
|
||||
message: 'ROT/RUT-rader kan inte periodiseras',
|
||||
})
|
||||
}
|
||||
if (item.quantity * item.unit_price <= 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['accrual_period_start'],
|
||||
message: 'Endast rader med positivt belopp kan periodiseras',
|
||||
})
|
||||
}
|
||||
}
|
||||
// Free-text rows skip the product-line requirements (description may be
|
||||
// empty for a spacer; quantity/unit/price are ignored).
|
||||
if (item.line_type === 'text') return
|
||||
@@ -485,6 +573,12 @@ export const CreateSupplierInvoiceItemSchema = z.object({
|
||||
quantity: z.number().optional(),
|
||||
unit: z.string().optional(),
|
||||
unit_price: z.number().optional(),
|
||||
// Periodisering (förutbetald kostnad): defer the line's net cost over the
|
||||
// service period. The registration entry debits the 17xx interim account
|
||||
// instead of account_number; input VAT is never deferred.
|
||||
accrual_period_start: isoDate.nullable().optional(),
|
||||
accrual_period_end: isoDate.nullable().optional(),
|
||||
accrual_balance_account: prepaidExpenseAccount.nullable().optional(),
|
||||
}).refine(
|
||||
(item) => {
|
||||
if (item.vat_amount == null) return true
|
||||
@@ -501,7 +595,21 @@ export const CreateSupplierInvoiceItemSchema = z.object({
|
||||
message: 'vat_amount cannot exceed line_total × vat_rate',
|
||||
path: ['vat_amount'],
|
||||
},
|
||||
)
|
||||
).superRefine((item, ctx) => {
|
||||
validateAccrualPeriod(item, ctx)
|
||||
if (item.accrual_period_start || item.accrual_period_end) {
|
||||
const lineTotal = item.amount != null
|
||||
? item.amount
|
||||
: (item.quantity ?? 1) * (item.unit_price ?? 0)
|
||||
if (lineTotal <= 0) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ['accrual_period_start'],
|
||||
message: 'Endast rader med positivt belopp kan periodiseras',
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const CreateSupplierInvoiceSchema = z.object({
|
||||
supplier_id: uuid,
|
||||
|
||||
Reference in New Issue
Block a user