Ctrl K
ComunidadGuías…

scripts/lib/year-end-undo-gate.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/**
 * Å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 }
}