* refactor: optimize page loading and data fetching * fix: resolve recurring production runtime errors * feat: add MCP company and customer updates * fix: handle year-end tax adjustments * feat: harden annual report compliance * fix: expand invoice logo and font support * fix: sanitize API route error responses * fix: sanitize user-facing error messages * feat: persist onboarding and tax assessment notices * fix: reduce cloud backup audit churn * feat: refine invoice editor layout * fix: show saved tax adjustments in INK2 * fix: complete annual report API mappings * docs: record operational safeguards and decisions * fix: harden annual report review findings * fix: adjust column span for description based on VAT registration * New css class name
145 lines
4.8 KiB
TypeScript
145 lines
4.8 KiB
TypeScript
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
|
|
export interface SignatureRequest {
|
|
id: string
|
|
user_id: string | null
|
|
company_id: string
|
|
fiscal_period_id: string
|
|
role: string
|
|
signer_name: string
|
|
status: 'pending' | 'signed' | 'declined'
|
|
signed_at: string | null
|
|
created_at: string
|
|
updated_at: string
|
|
annual_report_version_id: string | null
|
|
signing_method: 'paper_original' | 'advanced_e_signature' | 'bankid' | 'bolagsverket' | null
|
|
evidence_reference: string | null
|
|
evidence_recorded_at: string | null
|
|
}
|
|
|
|
export interface CreateSignatureRequestInput {
|
|
role: string
|
|
signer_name: string
|
|
}
|
|
|
|
/**
|
|
* List signature requests for a fiscal period's årsredovisning.
|
|
*/
|
|
export async function listSignatureRequests(
|
|
supabase: SupabaseClient,
|
|
companyId: string,
|
|
fiscalPeriodId: string,
|
|
): Promise<SignatureRequest[]> {
|
|
const { data, error } = await supabase
|
|
.from('arsredovisning_signature_requests')
|
|
.select('id, user_id, company_id, fiscal_period_id, role, signer_name, status, signed_at, created_at, updated_at, annual_report_version_id, signing_method, evidence_reference, evidence_recorded_at')
|
|
.eq('company_id', companyId)
|
|
.eq('fiscal_period_id', fiscalPeriodId)
|
|
.order('created_at', { ascending: true })
|
|
if (error) throw new Error(`Failed to list signature requests: ${error.message}`)
|
|
const rows = (data ?? []) as SignatureRequest[]
|
|
const unbound = rows.filter(
|
|
(request) => request.annual_report_version_id === null && request.status === 'pending',
|
|
)
|
|
if (unbound.length > 0) return unbound
|
|
const latestBound = [...rows]
|
|
.filter((request) => request.annual_report_version_id !== null)
|
|
.sort((left, right) => right.created_at.localeCompare(left.created_at))[0]
|
|
return latestBound
|
|
? rows.filter(
|
|
(request) => request.annual_report_version_id === latestBound.annual_report_version_id,
|
|
)
|
|
: rows
|
|
}
|
|
|
|
/**
|
|
* Create one signature request per styrelseledamot / VD. The BankID call
|
|
* itself isn't wired here: that ships in a follow-up that uses
|
|
* lib/auth/bankid helpers to sign and write the result back via
|
|
* markSignatureSigned(). For now this just records who is supposed to sign.
|
|
*/
|
|
export async function createSignatureRequest(
|
|
supabase: SupabaseClient,
|
|
companyId: string,
|
|
userId: string,
|
|
fiscalPeriodId: string,
|
|
input: CreateSignatureRequestInput,
|
|
): Promise<SignatureRequest> {
|
|
const { data, error } = await supabase
|
|
.from('arsredovisning_signature_requests')
|
|
.insert({
|
|
user_id: userId,
|
|
company_id: companyId,
|
|
fiscal_period_id: fiscalPeriodId,
|
|
role: input.role,
|
|
signer_name: input.signer_name,
|
|
status: 'pending',
|
|
})
|
|
.select('*')
|
|
.single()
|
|
if (error || !data) {
|
|
throw new Error(`Failed to create signature request: ${error?.message ?? 'unknown'}`)
|
|
}
|
|
return data as SignatureRequest
|
|
}
|
|
|
|
/**
|
|
* Mark a request as signed. Used by the (future) BankID completion handler.
|
|
* The DB trigger blocks further edits once signed, so this is a one-way
|
|
* transition.
|
|
*/
|
|
export async function markSignatureSigned(
|
|
supabase: SupabaseClient,
|
|
companyId: string,
|
|
requestId: string,
|
|
options: {
|
|
fiscalPeriodId: string
|
|
annualReportVersionId: string
|
|
signingMethod: SignatureRequest['signing_method']
|
|
evidenceReference: string
|
|
evidenceRecordedBy: string
|
|
signedAt?: string
|
|
bankidSignatureData?: Record<string, unknown>
|
|
},
|
|
): Promise<SignatureRequest> {
|
|
const { data, error } = await supabase
|
|
.from('arsredovisning_signature_requests')
|
|
.update({
|
|
status: 'signed',
|
|
signed_at: options.signedAt ?? new Date().toISOString(),
|
|
annual_report_version_id: options.annualReportVersionId,
|
|
signing_method: options.signingMethod,
|
|
evidence_reference: options.evidenceReference,
|
|
evidence_recorded_by: options.evidenceRecordedBy,
|
|
evidence_recorded_at: new Date().toISOString(),
|
|
bankid_signature_data: options.bankidSignatureData ?? null,
|
|
})
|
|
.eq('id', requestId)
|
|
.eq('company_id', companyId)
|
|
.eq('fiscal_period_id', options.fiscalPeriodId)
|
|
.eq('status', 'pending')
|
|
.or(
|
|
`annual_report_version_id.is.null,annual_report_version_id.eq.${options.annualReportVersionId}`,
|
|
)
|
|
.select('*')
|
|
.single()
|
|
if (error || !data) {
|
|
throw new Error(`Failed to mark signature signed: ${error?.message ?? 'unknown'}`)
|
|
}
|
|
return data as SignatureRequest
|
|
}
|
|
|
|
/**
|
|
* True when every signature request for the period is signed. The UI uses
|
|
* this to enable the "Ladda ner fastställd PDF" button and to gate filing.
|
|
*/
|
|
export async function isFullySignedOff(
|
|
supabase: SupabaseClient,
|
|
companyId: string,
|
|
fiscalPeriodId: string,
|
|
): Promise<boolean> {
|
|
const requests = await listSignatureRequests(supabase, companyId, fiscalPeriodId)
|
|
if (requests.length === 0) return false
|
|
return requests.every((r) => r.status === 'signed')
|
|
}
|