From c510bfab9e323873b1318c060f4b52b440c6b387 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Mon, 23 Feb 2026 16:15:46 +0100 Subject: [PATCH] fix: resolve infinite re-fetch loop in JournalEntryAttachments, update CLAUDE.md Fix onCountChange callback causing infinite fetch loop by using a ref instead of including it in useCallback deps. Also update CLAUDE.md with Zod validation docs, env vars, extension design doc reference, and API route patterns. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 23 ++++++++++++++++++- .../bookkeeping/JournalEntryAttachments.tsx | 9 +++++--- ...x.sql => 20240101000039_invoice_inbox.sql} | 0 3 files changed, 28 insertions(+), 4 deletions(-) rename supabase/migrations/{20240101000033_invoice_inbox.sql => 20240101000039_invoice_inbox.sql} (100%) diff --git a/CLAUDE.md b/CLAUDE.md index 9488e17e..d58211d1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,6 +87,9 @@ extensions/ Sector-based extension hierarchy sru-export/ SRU file export (top-level) lib/ + api/ Zod validation schemas and utilities for API routes + schemas.ts Zod schemas for all API request bodies and query params + validate.ts validateBody() and validateQuery() helpers bookkeeping/ Core journal entry engine and all entry generators engine.ts Draft/commit workflow, balance validation, voucher numbering invoice-entries.ts Sales invoice journal entries (supports per-line VAT rates) @@ -147,6 +150,8 @@ scripts/ Utility scripts (clear-user-data.sql, copy-extensions. move-extensions.js, setup-phase8.js) dev_docs/ Project documentation (BAS account guides, gap analysis, Enable Banking docs, Bokio reference screenshots) +extensions.md Extension system design document (architecture, data patterns, + sector model, workspace pattern, migration plan) ``` ### Key Relationships @@ -421,6 +426,8 @@ mockResult({ data: makeTransaction(), error: null }) ### Reference Tests +- `lib/api/__tests__/schemas.test.ts` — Zod schema validation +- `lib/api/__tests__/validate.test.ts` — Body/query validation helpers - `lib/bookkeeping/__tests__/engine.test.ts` — Balance validation - `lib/bookkeeping/__tests__/invoice-entries.test.ts` — Per-line VAT, mixed-rate invoices, credit notes - `lib/core/bookkeeping/__tests__/storno-service.test.ts` — Complex mock queues @@ -510,6 +517,8 @@ Standard pattern for new API routes: import { createClient } from '@/lib/supabase/server' import { NextResponse } from 'next/server' import { ensureInitialized } from '@/lib/init' +import { validateBody } from '@/lib/api/validate' +import { CreateInvoiceSchema } from '@/lib/api/schemas' ensureInitialized() // Module-level — loads extensions for event emission @@ -521,7 +530,12 @@ export async function POST(request: Request) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - // Business logic... + // Validate request body with Zod schema + const result = await validateBody(request, CreateInvoiceSchema) + if (!result.success) return result.response + const { data } = result + + // Business logic using validated `data`... // Always filter by user_id (defense in depth alongside RLS) // Wrap journal entry creation in try/catch (non-blocking side effect) // Emit events after successful operations @@ -532,6 +546,7 @@ export async function POST(request: Request) { **Key conventions**: - Call `ensureInitialized()` at module level in any route that emits events +- **Validate all input** with `validateBody()` / `validateQuery()` from `lib/api/validate.ts` using schemas from `lib/api/schemas.ts` - Dynamic route params use `{ params }: { params: Promise<{ id: string }> }` (Next.js 16) - Response shapes: `{ data }` for success, `{ error }` for failures - Journal entry creation is non-blocking: catch errors and continue @@ -582,13 +597,19 @@ Hosted on **Vercel** with cron jobs defined in `vercel.json`: NEXT_PUBLIC_SUPABASE_URL # Supabase project URL NEXT_PUBLIC_SUPABASE_ANON_KEY # Supabase anonymous key SUPABASE_SERVICE_ROLE_KEY # Supabase service role key +RESEND_API_KEY # Resend email service API key +RESEND_FROM_EMAIL # Sender email for transactional mail +RESEND_WEBHOOK_SECRET # Webhook auth for Resend ENABLE_BANKING_APP_ID # Enable Banking app ID ENABLE_BANKING_PRIVATE_KEY # Enable Banking private key (base64-encoded) +ENABLE_BANKING_SANDBOX # Enable Banking sandbox mode flag ANTHROPIC_API_KEY # Claude API key (ai-chat) OPENAI_API_KEY # OpenAI API key (embeddings) NEXT_PUBLIC_APP_URL # App base URL +CRON_SECRET # Auth secret for Vercel cron jobs NEXT_PUBLIC_VAPID_PUBLIC_KEY # Web push public key VAPID_PRIVATE_KEY # Web push private key +VAPID_SUBJECT # VAPID subject (mailto: URI) for web push ``` ## Other diff --git a/components/bookkeeping/JournalEntryAttachments.tsx b/components/bookkeeping/JournalEntryAttachments.tsx index 87b6d480..7533ef8d 100644 --- a/components/bookkeeping/JournalEntryAttachments.tsx +++ b/components/bookkeeping/JournalEntryAttachments.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect, useCallback } from 'react' +import { useState, useEffect, useCallback, useRef } from 'react' import { Button } from '@/components/ui/button' import { FileText, ImageIcon, Download, ChevronDown, ChevronUp, Plus } from 'lucide-react' import DocumentUploadZone from '@/components/bookkeeping/DocumentUploadZone' @@ -41,6 +41,9 @@ export default function JournalEntryAttachments({ const [showUpload, setShowUpload] = useState(false) const [uploadFiles, setUploadFiles] = useState([]) + const onCountChangeRef = useRef(onCountChange) + onCountChangeRef.current = onCountChange + const fetchDocuments = useCallback(async () => { try { const res = await fetch( @@ -48,13 +51,13 @@ export default function JournalEntryAttachments({ ) const { data } = await res.json() setDocuments(data || []) - onCountChange?.(data?.length || 0) + onCountChangeRef.current?.(data?.length || 0) } catch { console.error('Failed to fetch documents') } finally { setLoading(false) } - }, [journalEntryId, onCountChange]) + }, [journalEntryId]) useEffect(() => { fetchDocuments() diff --git a/supabase/migrations/20240101000033_invoice_inbox.sql b/supabase/migrations/20240101000039_invoice_inbox.sql similarity index 100% rename from supabase/migrations/20240101000033_invoice_inbox.sql rename to supabase/migrations/20240101000039_invoice_inbox.sql