c62321988b
* feat(reconciliation): underlag on a balansdag, the files behind a sign-off (Reko bilagor, PR 2) A konsult attaches the kontoutdrag, engagemangsbesked or reskontralista an account was reconciled against to (account_key, through_date), before or after the sign-off, from every account body on the Avstämning page. Rows live in account_reconciliation_attachments (append-only, removal stamp by trigger, RLS like account_reconciliations), bytes in the documents bucket under the company prefix so its RLS applies unchanged, and the full archive copies them into bilagor/ with a hash manifest. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz * fix(reconciliation): literal selects and payload in the attachments store so the phantom-column scanner can read them Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz * feat(bokslut): persisted closing checklist and missing-fiscal-year warning (Reko bilagor, PR 3) (#1867) * feat(bokslut): persisted closing checklist and missing-fiscal-year warning (Reko bilagor, PR 3) The bokslut checklist is a catalogue in code with one state row per period (bokslut_checklist_items): the steps the system can judge (sign-offs through balansdagen, reskontra tie-outs, drafts, voucher gaps, trial balance) are computed live and a stored row only overrides them; the manual steps are the konsult's ticks, with who and when. It sits on the wizard's Kontroll step and is dumped into the full archive. A hole between fiscal years (one-file SIE migrations) is now named on the bokslut readiness screen and on the import result screen, where the next file is one click away. Non-adjacent period links are #1849's fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz * fix(bokslut): count unexplained voucher gaps, literal select and payload for the checklist store Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvFveUpbdPBXdm7f5FEYoz --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
183 lines
7.0 KiB
TypeScript
183 lines
7.0 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import type { SupabaseClient } from '@supabase/supabase-js'
|
|
import { ISO_DATE_RE } from '@/lib/invariants'
|
|
import { createLogger } from '@/lib/logger'
|
|
import {
|
|
ALLOWED_DOCUMENT_TYPES,
|
|
DOCUMENTS_BUCKET,
|
|
computeSHA256,
|
|
validateDocumentFile,
|
|
validateDocumentMagicBytes,
|
|
} from '@/lib/core/documents/document-service'
|
|
import { parseAccountKey, type ReconciliationAttachment } from './schemas'
|
|
import {
|
|
getAttachmentRow,
|
|
insertAttachmentRow,
|
|
listAttachmentRows,
|
|
stampAttachmentRemoved,
|
|
toPublicAttachment,
|
|
type AttachmentRow,
|
|
} from './attachments-store'
|
|
|
|
const log = createLogger('reconciliation/attachments')
|
|
|
|
/**
|
|
* Underlag on a reconciliation balansdag: the bytes go to the company-scoped
|
|
* `documents` bucket (same validation and hashing as every other document in
|
|
* the archive), the row to account_reconciliation_attachments. The policy
|
|
* layer over attachments-store.ts: what may be attached, where it lives, and
|
|
* that removal is a stamp, never a delete.
|
|
*
|
|
* Storage keys start with `documents/<company_id>/` on purpose: the bucket's
|
|
* RLS grants INSERT/SELECT on the second path segment being one of the
|
|
* caller's companies, so the auth-bound client can upload without the
|
|
* service role. Reads for the inline route and the archive go through the
|
|
* service role after the row has authorized the caller.
|
|
*/
|
|
|
|
export const MAX_ATTACHMENT_NOTE_LENGTH = 500
|
|
|
|
export type AttachmentErrorCode = 'INVALID_ACCOUNT_KEY' | 'INVALID_DATE' | 'INVALID_FILE' | 'NOTE_TOO_LONG' | 'ALREADY_REMOVED'
|
|
|
|
export class ReconciliationAttachmentError extends Error {
|
|
readonly code: AttachmentErrorCode
|
|
constructor(message: string, code: AttachmentErrorCode) {
|
|
super(message)
|
|
this.name = 'ReconciliationAttachmentError'
|
|
this.code = code
|
|
}
|
|
}
|
|
|
|
function sanitizeFileName(name: string): string {
|
|
const trimmed = name.trim().replace(/[/\\]/g, '_').replace(/[ |