scripts/lib/year-end-undo-gate.ts
Rama main · réplica de lectura del repositorio original.
/**
* Årsredovisning gate for scripts/undo-year-end-closing.ts.
*
* Reopening a closed year is only lawful while the annual report built on it
* has no legal weight yet. What gives it weight, read from the schema:
*
* - a signature: arsredovisning_signature_requests.status = 'signed' (the
* signed_atomic CHECK ties signed_at to that status; both are checked so
* a hand-edited row cannot slip through);
* - a signed or filed version: annual_report_versions.status in
* ('signed', 'filed', 'registered'). 'signed' is only reachable once
* every bound signer has signed (enforce_annual_report_version_
* immutability), 'filed'/'registered' need a Bolagsverket receipt;
* - any arsredovisning_submissions row. Submissions are never deleted or
* cancelled (they close via status), so any row at all means the report
* has entered the filing pipeline and a human must look first.
*
* Adoption at the årsstämma (fastställelse) is not recorded as a distinct
* fact: arsredovisning_narratives.agm_date / agm_disposition_outcome are the
* draft content of the fastställelseintyg, typed in before anyone signs. They
* are surfaced as a warning so the operator confirms with the customer, but
* they do not block on their own.
*
* What does NOT block: pending or declined signature requests and draft or
* superseded versions. They are paperwork, not legal acts. A version still
* 'ready_for_signature' is returned in versionsToSupersede: the caller
* supersedes it (the same transition create_annual_report_version_with_
* signatures makes when the report is re-finalized) so its pending slots can
* no longer be signed against numbers the reopen is about to change
* (enforce_annual_report_signature_version_state refuses a signature on a
* version that is not ready_for_signature).
*/
export interface GateSubmission {
id: string
status: string
}
export interface GateSignatureRequest {
id: string
status: string
signed_at: string | null
annual_report_version_id: string | null
}
export interface GateVersion {
id: string
version_number: number
status: string
}
export interface YearEndUndoGateInput {
submissions: GateSubmission[]
signatureRequests: GateSignatureRequest[]
versions: GateVersion[]
/** arsredovisning_narratives.agm_date for the period, if any. */
agmDate: string | null
/** Today as YYYY-MM-DD (Swedish local date). */
today: string
}
export interface YearEndUndoGateResult {
/** Non-empty means refuse the reopen. */
blockers: string[]
warnings: string[]
/** ready_for_signature versions the reopen must supersede. */
versionsToSupersede: GateVersion[]
/** Pending requests bound to a version in versionsToSupersede. */
voidedRequests: GateSignatureRequest[]
}
const LEGALLY_BINDING_VERSION_STATUSES = new Set(['signed', 'filed', 'registered'])
export function evaluateYearEndUndoGate(input: YearEndUndoGateInput): YearEndUndoGateResult {
const blockers: string[] = []
const warnings: string[] = []
if (input.submissions.length > 0) {
const statuses = [...new Set(input.submissions.map((s) => s.status))].join(', ')
blockers.push(
`an årsredovisning submission exists for this period (status: ${statuses}): refuse to reopen`
)
}
const signed = input.signatureRequests.filter((r) => r.status === 'signed' || r.signed_at !== null)
if (signed.length > 0) {
blockers.push(
`${signed.length} årsredovisning signature(s) already signed for this period: refuse to reopen`
)
}
const binding = input.versions.filter((v) => LEGALLY_BINDING_VERSION_STATUSES.has(v.status))
for (const v of binding) {
blockers.push(`årsredovisning version ${v.version_number} is ${v.status}: refuse to reopen`)
}
const versionsToSupersede = input.versions.filter((v) => v.status === 'ready_for_signature')
const supersededIds = new Set(versionsToSupersede.map((v) => v.id))
const pending = input.signatureRequests.filter((r) => r.status === 'pending' && r.signed_at === null)
const voidedRequests = pending.filter(
(r) => r.annual_report_version_id !== null && supersededIds.has(r.annual_report_version_id)
)
const unbound = pending.filter((r) => r.annual_report_version_id === null)
const declined = input.signatureRequests.filter((r) => r.status === 'declined')
if (voidedRequests.length > 0) {
warnings.push(
`${voidedRequests.length} pending signature request(s) are bound to a version that will be ` +
'superseded; they can no longer be signed and the next finalize prepares fresh slots'
)
}
if (unbound.length > 0) {
warnings.push(
`${unbound.length} unbound pending signer slot(s) kept as the roster for the next finalize`
)
}
if (declined.length > 0) {
warnings.push(`${declined.length} declined signature request(s) kept as evidence`)
}
if (input.agmDate && input.agmDate <= input.today) {
warnings.push(
`an årsstämma date (${input.agmDate}) is recorded for this period: confirm with the customer ` +
'that no stämma adopted the årsredovisning before reopening'
)
}
return { blockers, warnings, versionsToSupersede, voidedRequests }
}