Files
accounted/lib/bokslut/arsredovisning/model.ts
T
Mattsson 466e55a015 Fix/invoice delivery and payment accounts (#1116)
* fix: reconcile annual reports with final closing entries

* test: cover annual report depreciation and VAT balances

* Merge remote-tracking branch 'origin/main' into fix/usr-fdbck-ch

* fix: show exact invoice delivery details

* fix: use currency account in invoice emails

* fix: address invoice delivery review feedback

* fix: harden invoice delivery and payment accounts

* test: assert RLS-denied zero-row updates

* fix: close remaining invoice compliance gaps

* fix: harden invoice archive authorization

* fix: close invoice delivery review findings

* fix: verify delivery finalization results

* fix: cap combined invoice email recipients

* fix: close final invoice compliance findings

* fix: prevent stale payment account saves

* test: prove invoice delivery isolation

* fix: close invoice privacy review findings

* test: normalize delivery retention dates
2026-07-23 09:54:02 +02:00

165 lines
5.5 KiB
TypeScript

import type { SupabaseClient } from '@supabase/supabase-js'
import { buildArsredovisningData } from './build-data'
import { listSignatureRequests } from './signature-service'
import { getAnnualReportProfile } from './profile-service'
import { evaluateAnnualReportEligibility } from './eligibility'
import {
validateAnnualReportCompleteness,
validateStatementIntegrity,
} from './completeness'
import {
ANNUAL_REPORT_SCHEMA_VERSION,
type AnnualReportDisclosureState,
type AnnualReportSizeMetrics,
type AnnualReportValidationStage,
type CanonicalAnnualReport,
} from './compliance-types'
import { buildIxbrlInput, type BuildIxbrlOptions } from '@/lib/bokslut/ixbrl/build-input'
import { computeMedelantalAnstallda } from '@/lib/salary/medelantal'
export interface BuildCanonicalAnnualReportOptions extends BuildIxbrlOptions {
stage?: AnnualReportValidationStage
generatedAt?: string
includeIxbrl?: boolean
}
interface EmployeeRow {
employment_start: string
employment_end: string | null
employment_degree: number
}
function reportMetrics(
report: Awaited<ReturnType<typeof buildArsredovisningData>>,
employees: EmployeeRow[],
): AnnualReportSizeMetrics {
const currentOverview = report.forvaltningsberattelse.flerarsoversikt.find(
(row) => row.year === report.fiscal_period.name,
)
const previousOverview = report.previous_period
? report.forvaltningsberattelse.flerarsoversikt.find(
(row) => row.year === report.previous_period?.name,
)
: null
return {
current: {
employees: computeMedelantalAnstallda(
employees,
report.fiscal_period.period_start,
report.fiscal_period.period_end,
),
balance_sheet_total: report.balansrakning.total_assets,
net_revenue: currentOverview?.net_revenue ?? null,
},
previous: report.previous_period
? {
employees: computeMedelantalAnstallda(
employees,
report.previous_period.period_start,
report.previous_period.period_end,
),
balance_sheet_total: report.balansrakning.total_assets_previous,
net_revenue: previousOverview?.net_revenue ?? null,
}
: null,
}
}
function disclosureState(
report: Awaited<ReturnType<typeof buildArsredovisningData>>,
): AnnualReportDisclosureState {
return {
long_term_debt_over_five_years_confirmed:
report.disclosures.confirmations.long_term_debt_over_five_years,
securities_pledged_confirmed: report.disclosures.confirmations.securities_pledged,
contingent_liabilities_confirmed:
report.disclosures.confirmations.contingent_liabilities,
parent_company_confirmed: report.disclosures.confirmations.parent_company,
agm_disposition_outcome: report.forvaltningsberattelse.agm_disposition_outcome,
agm_disposition_decision: report.forvaltningsberattelse.agm_disposition_decision,
}
}
export async function buildCanonicalAnnualReport(
supabase: SupabaseClient,
companyId: string,
fiscalPeriodId: string,
options: BuildCanonicalAnnualReportOptions = {},
): Promise<CanonicalAnnualReport> {
const [report, profile, signatures, employeesResult] = await Promise.all([
buildArsredovisningData(supabase, companyId, fiscalPeriodId),
getAnnualReportProfile(supabase, companyId, fiscalPeriodId),
listSignatureRequests(supabase, companyId, fiscalPeriodId),
supabase
.from('employees')
.select('employment_start, employment_end, employment_degree')
.eq('company_id', companyId),
])
if (employeesResult.error) {
throw new Error(`Failed to load annual report employee metrics: ${employeesResult.error.message}`)
}
const activeSignatures = signatures.filter((signature) => signature.status !== 'declined')
report.signatures = activeSignatures.map((signature) => ({
role: signature.role,
name: signature.signer_name,
signed_at: signature.signed_at,
}))
const metrics = reportMetrics(report, (employeesResult.data ?? []) as EmployeeRow[])
const eligibility = evaluateAnnualReportEligibility({
entityType: report.company.entity_type,
framework: report.accounting_framework,
periodStart: report.fiscal_period.period_start,
periodEnd: report.fiscal_period.period_end,
profile,
metrics,
})
const disclosures = disclosureState(report)
let validation = validateAnnualReportCompleteness({
report,
profile,
disclosures,
eligibility,
stage: options.stage ?? 'draft',
todayIso: options.todayIso,
})
let ixbrl = null
if (report.accounting_framework === 'k2' && options.includeIxbrl !== false) {
ixbrl = await buildIxbrlInput(supabase, companyId, fiscalPeriodId, {
undertecknare: options.undertecknare,
proposedDividend: options.proposedDividend,
todayIso: options.todayIso,
reportData: report,
signatureRequests: activeSignatures,
})
}
const crossDocumentIssues = validateStatementIntegrity(report, ixbrl).filter(
(issue) => issue.code === 'AR-IXBRL-STATEMENT-MISMATCH',
)
if (crossDocumentIssues.length > 0) {
validation = {
...validation,
ok: false,
error_count: validation.error_count + crossDocumentIssues.length,
issues: [...validation.issues, ...crossDocumentIssues],
}
}
return {
schema_version: ANNUAL_REPORT_SCHEMA_VERSION,
generated_at: options.generatedAt ?? new Date().toISOString(),
company_id: companyId,
fiscal_period_id: fiscalPeriodId,
entity_type: report.company.entity_type,
report,
profile,
disclosures,
eligibility,
validation,
ixbrl,
}
}