diff --git a/.claude/skills/create-ticket/SKILL.md b/.claude/skills/create-ticket/SKILL.md new file mode 100644 index 00000000..2b55cdac --- /dev/null +++ b/.claude/skills/create-ticket/SKILL.md @@ -0,0 +1,158 @@ +--- +name: create-ticket +description: "Create a detailed Linear ticket from a prompt. Asks 5 clarifying questions one at a time, scans the codebase, previews the ticket, and creates it via Linear MCP. Supports prompt templates: /create-ticket /security, /create-ticket /design , /create-ticket ." +--- + +# Create Ticket + +You are a ticket creation assistant for gnubok. Your job is to take a rough idea (bug, feature, or improvement) and turn it into a well-defined, actionable Linear ticket through structured conversation. + +## Step 1: Parse Input + +The user invokes this skill as `/create-ticket `. + +The input can be: +- **Free text only**: `/create-ticket I want to add bulk PDF export to invoices` +- **Prompt template only**: `/create-ticket /security` +- **Prompt template + free text**: `/create-ticket /design the settings page feels inconsistent` + +### Detect prompt template + +Check if the input starts with a `/` followed by a keyword. Map it to a prompt file: + +| Keyword | File | +|---------|------| +| `/security` | `prompts/security.md` | +| `/design` | `prompts/design.md` | +| `/database` | `prompts/database.md` | +| `/performance` | `prompts/performance.md` | +| `/bookkeeping` | `prompts/bookkeeping.md` | + +If a prompt template is detected: +1. Read the corresponding file from the `prompts/` directory relative to this skill file. +2. Use its **Perspective**, **Checklist**, and **Classification** sections as context for the entire flow. +3. Treat any remaining text after the keyword as scoping context (e.g., "the settings page", "invoice API routes"). + +If no prompt template is detected, treat the entire input as a free-text description. + +If a prompt template keyword is used but doesn't match any file, tell the user the available templates and ask them to pick one or provide free text instead. + +## Step 2: Ask 5 Clarifying Questions + +Analyze the input (and prompt template if loaded) to generate 5 questions that will clarify the ticket. Ask them **one at a time** — wait for the user's answer before asking the next question. + +### Question generation guidelines + +- Questions must be **dynamic** — tailored to the specific prompt, not generic. +- Each question should build on previous answers when relevant. +- Cover these dimensions across the 5 questions (adapt wording to the context): + 1. **Problem clarity** — What exactly is wrong, missing, or needed? (dig deeper than the initial prompt) + 2. **Impact & scope** — Who is affected? How often? What's the severity? + 3. **Desired outcome** — What should it look like when done? What's the acceptance criteria? + 4. **Constraints** — Are there technical, legal, or timeline constraints? + 5. **Location** — Where in the app would this change be implemented? (the user can say "not sure" and you'll figure it out in Step 3) +- If a prompt template is loaded, frame questions through that lens (e.g., security questions for `/security`, design questions for `/design`). +- Keep questions concise and specific. Avoid open-ended questions like "anything else?" + +### Format + +Ask each question as a single, clear message. Example: + +> **Question 1/5**: You mentioned the invoice PDF export is missing — is this about exporting a single invoice as PDF (which already exists) or bulk-exporting multiple invoices at once? + +## Step 3: Codebase Scan + +After all 5 questions are answered, scan the codebase to identify the specific files and components that would be affected by this change. + +- Use `Glob` and `Grep` to find relevant files based on the answers. +- If a prompt template is loaded, use its checklist to guide what you scan for. +- If the user specified a location, start there. If they said "not sure", use the context from all answers to determine the right area. +- Identify **specific file paths with line numbers** where changes would need to happen. +- Note any related files (tests, types, API routes) that would also be affected. + +## Step 4: Preview Ticket + +Compose the full ticket and present it to the user for approval before creating it. + +### Auto-detect ticket type + +Based on the problem and solution from the conversation: +- **Bug**: Something is broken, produces wrong results, or doesn't work as expected. +- **Feature**: Something entirely new that doesn't exist yet. +- **Improvement**: Something exists but could be better (refactor, optimization, UX enhancement, hardening). + +### Infer priority + +Based on the severity and impact discussed: +- **Urgent (1)**: System is broken, data loss risk, security vulnerability, compliance violation. +- **High (2)**: Major functionality affected, blocks users, significant UX regression. +- **Medium (3)**: Noticeable issue but workaround exists, moderate UX impact, useful enhancement. +- **Low (4)**: Minor polish, nice-to-have, cosmetic, non-blocking improvement. + +### Ticket format + +Present the preview in this format: + +``` +## Ticket Preview + +**Title**: [{Area}] {Short actionable title} +**Type**: Bug / Feature / Improvement +**Priority**: Urgent / High / Medium / Low + +--- + +## Problem +{What's wrong or what's missing — current state. Be specific with concrete examples.} + +## Solution +{What should be built or fixed — desired state. Be specific about the expected behavior.} + +## Why +{Why this matters — business impact, UX impact, compliance requirement, or technical justification.} + +## Where +{Affected files and components with paths and line numbers.} + +**Files:** +- `path/to/file.ts:L42` — {what changes here} +- `path/to/other.ts:L15` — {what changes here} + +**Related files:** +- `path/to/test.ts` — tests to update +- `types/index.ts` — types to add/modify + +{Any additional implementation guidance.} + +--- +*Generated by /create-ticket* +``` + +After the preview, ask: + +> Create this ticket in Linear? (yes / no / edit) + +- **yes**: Proceed to Step 5. +- **no**: Cancel — do not create the ticket. +- **edit**: Ask what they want to change, update the preview, and ask again. + +## Step 5: Create Linear Ticket + +Create the ticket using `mcp__claude_ai_Linear__save_issue` with: + +- **team**: `Gnubok` +- **title**: The title from the preview (keep under 70 characters) +- **description**: The full description from the preview (Problem, Solution, Why, Where sections) +- **labels**: The auto-detected type — `Bug`, `Feature`, or `Improvement` +- **priority**: The inferred priority number (1-4) + +After creation, report the Linear ticket identifier (e.g., `GNO-123`) so the user can reference it. + +## Important Notes + +- Be specific and actionable. Vague tickets waste everyone's time. +- Include real file paths and line numbers in the "Where" section — never guess, always scan. +- Keep the title short and prefixed with the area in brackets. +- The description should be detailed enough that someone could pick up the ticket and start working without additional context. +- If using a prompt template, the ticket should reflect that lens — a `/security` ticket should frame the problem in security terms, a `/design` ticket in design terms. +- Do not create the ticket without explicit user approval. diff --git a/.claude/skills/create-ticket/prompts/bookkeeping.md b/.claude/skills/create-ticket/prompts/bookkeeping.md new file mode 100644 index 00000000..267ca3af --- /dev/null +++ b/.claude/skills/create-ticket/prompts/bookkeeping.md @@ -0,0 +1,62 @@ +# Bookkeeping Prompt + +## Perspective + +You are scanning for Swedish accounting compliance issues, bookkeeping logic gaps, and financial data handling problems. gnubok implements double-entry bookkeeping compliant with Bokforingslagen (BFL) and BFN standards. Focus on correctness of journal entries, VAT handling, account mappings, and legal guardrails. + +## Checklist + +### Journal Entry Integrity +- [ ] All journal entries route through `createJournalEntry()` in `lib/bookkeeping/engine.ts` +- [ ] Entries balance: `sum(debits) === sum(credits)`, both `> 0` +- [ ] Account numbers are strings (`'1930'`, never `1930`) +- [ ] Monetary calculations use `Math.round(x * 100) / 100` (never `toFixed()`) +- [ ] Voucher numbers assigned via DB RPC (never set manually) +- [ ] Posted entries are never edited (storno pattern for corrections) +- [ ] `reverseEntry()` used for cancellation, `correctEntry()` for corrections + +### VAT Handling +- [ ] Correct VAT treatment applied per transaction type (`standard_25`, `reduced_12`, `reduced_6`, `reverse_charge`, `export`, `exempt`) +- [ ] Mixed-rate invoices handled via `generatePerRateLines()` in `invoice-entries.ts` +- [ ] `getAvailableVatRates()` used to determine valid rates based on customer type +- [ ] Output VAT mapped to correct accounts (2611/2621/2631 for 25%/12%/6%) +- [ ] Input VAT on 2641, calculated input VAT (EU reverse charge) on 2645 +- [ ] EU services on 3308, export on 3305 +- [ ] VAT declaration rutor (SKV 4700) correctly calculated + +### BAS Account Mappings +- [ ] Revenue accounts correct: 3001 (25%), 3002 (12%), 3003 (6%) +- [ ] 1510 for accounts receivable, 2440 for accounts payable +- [ ] 1930 for business bank account +- [ ] 2013 for private withdrawals (enskild firma), 2893 for shareholder loan (aktiebolag) +- [ ] Account used matches the entity type (EF vs AB) + +### Entity Type Handling +- [ ] Enskild firma vs aktiebolag differences respected +- [ ] Private withdrawals (2013) only for EF +- [ ] Shareholder loan (2893) only for AB +- [ ] Tax reporting differences handled (INK1 vs INK2) + +### Period & Fiscal Year +- [ ] Period lock enforcement respected (cannot post to closed/locked periods) +- [ ] Fiscal year boundaries correct +- [ ] Year-end closing entries follow Swedish standards +- [ ] Opening balances carried forward correctly + +### Reports & Declarations +- [ ] Trial balance sums match journal entries +- [ ] VAT declaration rutor map correctly to BAS accounts +- [ ] Income statement categories correct +- [ ] NE-bilaga / INK2 / SRU export in correct format +- [ ] Reports filter by fiscal year and user + +### Document Retention +- [ ] 7-year retention enforced on documents linked to posted entries +- [ ] Receipts and attachments cannot be deleted after entry is posted +- [ ] Archive export includes all legally required documents + +## Classification + +- **Bug**: Wrong account mapping, VAT calculated incorrectly, balance check missing, posted entry can be modified, period lock bypassed, retention trigger missing. +- **Feature**: New journal entry type needed, new report, new VAT treatment, new entity type support. +- **Improvement**: Better validation on account selection, clearer error message on balance failure, edge case in VAT calculation not handled, report could include additional breakdown. diff --git a/.claude/skills/create-ticket/prompts/database.md b/.claude/skills/create-ticket/prompts/database.md new file mode 100644 index 00000000..fc07a4fb --- /dev/null +++ b/.claude/skills/create-ticket/prompts/database.md @@ -0,0 +1,64 @@ +# Database Prompt + +## Perspective + +You are scanning for database schema, migration, and query issues in a Supabase PostgreSQL application with RLS. Focus on data integrity, performance, security policies, and compliance with Swedish accounting law (BFL 7-year retention, period locks, immutable posted entries). + +## Checklist + +### Schema Design +- [ ] Tables have UUID primary keys (`DEFAULT uuid_generate_v4()`) +- [ ] `user_id UUID REFERENCES auth.users ON DELETE CASCADE NOT NULL` on all user-owned tables +- [ ] `updated_at` trigger using `update_updated_at_column()` on all tables +- [ ] Appropriate NOT NULL constraints on required fields +- [ ] CHECK constraints for enum-like fields (status, type columns) +- [ ] Foreign keys with appropriate ON DELETE behavior (CASCADE vs RESTRICT) + +### Row Level Security +- [ ] RLS enabled on every table +- [ ] SELECT policy: `auth.uid() = user_id` +- [ ] INSERT policy: `auth.uid() = user_id` +- [ ] UPDATE policy: `auth.uid() = user_id` +- [ ] No overly permissive policies (e.g., `true` for authenticated users) +- [ ] Service role access justified where used + +### Indexes & Performance +- [ ] Indexes on foreign key columns used in JOINs +- [ ] Indexes on columns used in WHERE clauses (especially `user_id`, `status`, `date`) +- [ ] Composite indexes for common multi-column queries +- [ ] No missing indexes on large tables causing sequential scans +- [ ] No unnecessary indexes adding write overhead + +### Migrations +- [ ] New migrations don't modify existing migration files +- [ ] Migrations are idempotent where possible (`IF NOT EXISTS`) +- [ ] Enforcement triggers (migration 017) never modified +- [ ] Timestamp-based naming for new migrations +- [ ] Backwards-compatible changes (additive, not destructive) + +### Data Integrity +- [ ] Posted journal entries are immutable (enforced by trigger) +- [ ] Voucher numbers are sequential (assigned via DB RPC, never manually) +- [ ] Period lock enforcement prevents writes to closed periods +- [ ] 7-year document retention trigger prevents deletion +- [ ] Monetary values stored as numeric/decimal (not float) + +### Queries (Application Layer) +- [ ] Queries filter by `user_id` as defense-in-depth alongside RLS +- [ ] No N+1 query patterns (batch fetches instead) +- [ ] `.select()` specifies columns (not `select('*')` on wide tables) +- [ ] Supabase `.single()` used when expecting one row +- [ ] Error handling on all database operations +- [ ] Transactions used for multi-step mutations + +### Compliance +- [ ] Account numbers stored as strings (`'1930'`, not `1930`) +- [ ] Monetary calculations use `Math.round(x * 100) / 100` (not `toFixed()`) +- [ ] All journal entries route through the bookkeeping engine (never direct inserts) +- [ ] Storno pattern used for corrections (never edit posted entries) + +## Classification + +- **Bug**: Missing RLS policy, data integrity violation possible, missing NOT NULL allowing bad data, incorrect ON DELETE behavior, broken trigger. +- **Feature**: New table needs migration, new index for a new query pattern, new RPC function needed. +- **Improvement**: Missing index on existing table, overly broad SELECT, N+1 query could be batched, schema could use a CHECK constraint. diff --git a/.claude/skills/create-ticket/prompts/design.md b/.claude/skills/create-ticket/prompts/design.md new file mode 100644 index 00000000..a995592a --- /dev/null +++ b/.claude/skills/create-ticket/prompts/design.md @@ -0,0 +1,60 @@ +# Design Prompt + +## Perspective + +You are scanning for design and UX issues in the gnubok interface. The app follows a minimal, sharp, efficient aesthetic inspired by Mercury (banking). Evaluate against the gnubok design system: grayscale palette with sage green/terracotta/ochre accents, Fraunces headings, Geist body, generous whitespace, subtle motion. + +## Checklist + +### Consistency & Design System +- [ ] Spacing values use Tailwind scale (not arbitrary values) +- [ ] Colors are from the design palette (grayscale, sage green, terracotta, ochre) +- [ ] Fraunces used for display headings, Geist for body text +- [ ] `tabular-nums` applied to all financial/numeric data +- [ ] shadcn/ui components used where appropriate +- [ ] Icon sizes consistent (15px nav, larger for empty states) +- [ ] Border styles consistent (subtle, 60% opacity) + +### Loading & Empty States +- [ ] Pages have loading states (skeletons preferred over spinners) +- [ ] Empty states exist with message and CTA when no data +- [ ] Loading states match the layout of the loaded content + +### Error Handling UI +- [ ] Error boundaries or error states shown to the user +- [ ] Forms show inline validation errors +- [ ] Error messages are helpful and in Swedish + +### Animation & Motion +- [ ] List items stagger-animate on entry +- [ ] Interactive elements have hover/active transitions +- [ ] Transitions use appropriate easing (spring for feedback, ease for reveals) +- [ ] `prefers-reduced-motion` respected +- [ ] No abrupt state changes that need transitions + +### Accessibility +- [ ] Visible focus rings on interactive elements +- [ ] WCAG AA contrast (4.5:1 text, 3:1 UI components) +- [ ] Color never sole indicator of state (paired with icons/text/shape) +- [ ] Form inputs labeled (label element or aria-label) +- [ ] Touch targets large enough + +### Layout & Responsiveness +- [ ] Layout works on mobile widths +- [ ] Tables horizontally scrollable on small screens +- [ ] Whitespace generous but not wasteful +- [ ] Dense data uses tighter but non-cramped spacing + +### Polish & Details +- [ ] Numbers right-aligned in tables +- [ ] Monetary values formatted consistently (Swedish format with kr) +- [ ] Dates formatted consistently +- [ ] Positive/negative amounts visually distinct +- [ ] Interactive elements obviously interactive (cursor, hover state) +- [ ] Disabled states visually clear + +## Classification + +- **Bug**: Broken layout, invisible text (contrast fail), non-functional interactive element, inaccessible form. +- **Feature**: Missing empty state, missing loading skeleton, missing responsive breakpoint, missing animation. +- **Improvement**: Inconsistent spacing, off-palette color, missing tabular-nums, hover state could be smoother, better icon choice. diff --git a/.claude/skills/create-ticket/prompts/performance.md b/.claude/skills/create-ticket/prompts/performance.md new file mode 100644 index 00000000..19112777 --- /dev/null +++ b/.claude/skills/create-ticket/prompts/performance.md @@ -0,0 +1,56 @@ +# Performance Prompt + +## Perspective + +You are scanning for performance issues in a Next.js 16 (App Router) + React 19 + Supabase application. Focus on page load speed, rendering efficiency, bundle size, data fetching patterns, and perceived performance. The app targets short, focused sessions (90 seconds) — speed is a core feature. + +## Checklist + +### Rendering & React +- [ ] Components that don't need interactivity are Server Components (no unnecessary `'use client'`) +- [ ] Large lists use virtualization or pagination (not rendering 1000+ items) +- [ ] Expensive computations wrapped in `useMemo` where re-renders are frequent +- [ ] Callback functions stable with `useCallback` when passed as props to memoized children +- [ ] No unnecessary re-renders from unstable object/array references in props or context +- [ ] Suspense boundaries with appropriate fallbacks for async components + +### Data Fetching +- [ ] Server Components fetch data on the server (not client-side `useEffect` for initial data) +- [ ] No waterfall fetches (parallel when independent) +- [ ] Pagination or cursor-based loading for large datasets +- [ ] No fetching data that's already available from a parent component +- [ ] API routes return only needed fields (not entire rows with unused columns) +- [ ] Appropriate use of `revalidatePath` / `revalidateTag` for cache invalidation + +### Bundle Size +- [ ] Heavy libraries imported dynamically (`next/dynamic`) where not needed on initial load +- [ ] No duplicate dependencies (same library imported from different paths) +- [ ] Tree-shaking friendly imports (`import { X } from 'lib'` not `import * as lib`) +- [ ] Images optimized with `next/image` (not raw `` tags) +- [ ] SVG icons from Lucide imported individually (not the entire icon set) + +### Database & API +- [ ] Queries have appropriate indexes (see database prompt for details) +- [ ] No N+1 patterns (fetching related data in loops) +- [ ] Batch operations used where possible (bulk insert/update) +- [ ] API responses are reasonably sized (not returning megabytes of data) +- [ ] Rate limiting in place for expensive operations + +### Perceived Performance +- [ ] Loading skeletons match content layout (not generic spinners) +- [ ] Optimistic UI updates for user actions (don't wait for server response to show feedback) +- [ ] Transitions between states are smooth (no jarring layout shifts) +- [ ] Critical content above the fold loads first +- [ ] Non-critical content lazy-loaded or deferred + +### Caching +- [ ] Static pages/routes use appropriate caching headers +- [ ] API responses include cache headers where data doesn't change frequently +- [ ] Client-side state management avoids redundant fetches +- [ ] Supabase real-time subscriptions used only where needed (not as a polling replacement) + +## Classification + +- **Bug**: Memory leak, infinite re-render loop, blocking the main thread, N+1 causing timeouts. +- **Feature**: Needs pagination, needs virtualization, needs caching layer, needs optimistic updates. +- **Improvement**: Unnecessary `'use client'`, could use Server Component, missing `useMemo` on expensive computation, bundle could be split, fetch could be parallelized. diff --git a/.claude/skills/create-ticket/prompts/security.md b/.claude/skills/create-ticket/prompts/security.md new file mode 100644 index 00000000..af67a505 --- /dev/null +++ b/.claude/skills/create-ticket/prompts/security.md @@ -0,0 +1,59 @@ +# Security Prompt + +## Perspective + +You are scanning for security vulnerabilities and hardening opportunities in a Next.js 16 + Supabase application. Focus on OWASP Top 10, authentication/authorization gaps, data exposure, and Swedish compliance requirements (GDPR, BFL 7-year retention). + +## Checklist + +### Authentication & Authorization +- [ ] All API routes check `supabase.auth.getUser()` and return 401 if missing +- [ ] MFA enforcement where required (`NEXT_PUBLIC_REQUIRE_MFA`) +- [ ] API key routes validate via `validate_and_increment_api_key` RPC +- [ ] No auth bypass in middleware exclusion patterns + +### Row Level Security +- [ ] All tables have RLS enabled +- [ ] Policies use `auth.uid() = user_id` (not broader conditions) +- [ ] API routes apply defense-in-depth `user_id` filtering alongside RLS +- [ ] Service role client usage is justified and scoped + +### Input Validation +- [ ] All API route bodies validated via `validateBody()` with Zod schemas +- [ ] Dynamic route params validated (UUID format, type checks) +- [ ] No raw user input in SQL queries (parameterized only) +- [ ] File upload types and sizes validated + +### Data Exposure +- [ ] API responses don't leak sensitive fields (passwords, tokens, internal IDs) +- [ ] Error responses don't expose stack traces or internal details +- [ ] Supabase `.select()` calls specify columns (not `select('*')` with sensitive data) +- [ ] Logs don't contain PII or secrets + +### Injection & XSS +- [ ] No `dangerouslySetInnerHTML` without sanitization +- [ ] No string interpolation in SQL (use parameterized queries) +- [ ] No `eval()`, `Function()`, or dynamic code execution +- [ ] URL parameters are validated before use + +### CSRF & Headers +- [ ] State-changing operations use POST/PUT/DELETE (not GET) +- [ ] CORS configured appropriately +- [ ] Security headers set (CSP, X-Frame-Options, etc.) + +### Secrets & Configuration +- [ ] No hardcoded secrets, API keys, or credentials in source +- [ ] Environment variables used for all sensitive config +- [ ] `.env` files in `.gitignore` +- [ ] OAuth secrets properly encrypted (AES-256-GCM) + +### Compliance (Swedish Law) +- [ ] 7-year document retention enforced (cannot delete posted entries) +- [ ] Period lock enforcement (cannot write to locked periods) +- [ ] Audit trail integrity (voucher numbers sequential, entries immutable) + +## Classification + +- **Bug**: Active vulnerability — missing auth check, SQL injection vector, data leak, RLS gap, exposed secret. +- **Feature**: New security capability needed — audit logging, rate limiting on a new endpoint, CSP policy addition. +- **Improvement**: Hardening — adding input validation to an endpoint that works but accepts too broadly, tightening a SELECT to specific columns, adding rate limiting. diff --git a/extensions/general/enable-banking/components/BankingSettingsPanel.tsx b/extensions/general/enable-banking/components/BankingSettingsPanel.tsx index 507f7e48..27bdf0e3 100644 --- a/extensions/general/enable-banking/components/BankingSettingsPanel.tsx +++ b/extensions/general/enable-banking/components/BankingSettingsPanel.tsx @@ -1,6 +1,6 @@ 'use client' -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import Link from 'next/link' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' @@ -26,6 +26,7 @@ export default function BankingSettingsPanel() { const [syncingConnectionId, setSyncingConnectionId] = useState(null) const [isConnecting, setIsConnecting] = useState(false) const [connectingBankName, setConnectingBankName] = useState(null) + const connectingRef = useRef(false) const [isLoading, setIsLoading] = useState(true) const [showCsvFallback, setShowCsvFallback] = useState(false) @@ -49,6 +50,8 @@ export default function BankingSettingsPanel() { } async function handleConnectBank(bank: Bank) { + if (connectingRef.current) return + connectingRef.current = true setIsConnecting(true) setConnectingBankName(bank.name) @@ -92,6 +95,7 @@ export default function BankingSettingsPanel() { description: error instanceof Error ? error.message : 'Kunde inte ansluta bank', variant: 'destructive', }) + connectingRef.current = false setIsConnecting(false) setConnectingBankName(null) setShowCsvFallback(true) diff --git a/extensions/general/enable-banking/index.ts b/extensions/general/enable-banking/index.ts index 83320c30..038a5963 100644 --- a/extensions/general/enable-banking/index.ts +++ b/extensions/general/enable-banking/index.ts @@ -101,18 +101,37 @@ export const enableBankingExtension: Extension = { psu_type: psuType, }) - // Clean up any stale pending connections for this user+bank - // to avoid conflicts with the new authorization - const { data: staleConnections } = await supabase + // Reject if there's already a recent pending connection for this user+bank + // to prevent double-click race conditions that confuse the bank's consent flow + const { data: recentPending } = await supabase .from('bank_connections') - .select('id') + .select('id, created_at') .eq('user_id', user.id) .eq('bank_name', aspsp_name) .eq('status', 'pending') + .order('created_at', { ascending: false }) + .limit(1) + .maybeSingle() - if (staleConnections && staleConnections.length > 0) { + if (recentPending) { + const pendingAge = Date.now() - new Date(recentPending.created_at).getTime() + const STALE_THRESHOLD_MS = 5 * 60 * 1000 // 5 minutes + + if (pendingAge < STALE_THRESHOLD_MS) { + log.info('[enable-banking] Rejecting duplicate connect — recent pending exists', { + existing_id: recentPending.id, + age_ms: pendingAge, + }) + return NextResponse.json( + { error: 'En anslutning pågår redan. Vänta och försök igen.' }, + { status: 409 } + ) + } + + // Clean up stale pending connections (older than threshold) log.info('[enable-banking] Cleaning up stale pending connections', { - count: staleConnections.length, + stale_id: recentPending.id, + age_ms: pendingAge, }) await supabase .from('bank_connections') diff --git a/supabase/migrations/20260330120000_add_last_integrity_check_at.sql b/supabase/migrations/20260330120000_add_last_integrity_check_at.sql new file mode 100644 index 00000000..7f140210 --- /dev/null +++ b/supabase/migrations/20260330120000_add_last_integrity_check_at.sql @@ -0,0 +1,11 @@ +-- Add last_integrity_check_at column for document integrity verification cron +-- Tracks when each document was last verified, allowing the cron to prioritize +-- unchecked or least-recently-checked documents. + +ALTER TABLE public.document_attachments + ADD COLUMN last_integrity_check_at timestamptz; + +-- Index for efficient ordering in the verification cron (nulls first = unchecked prioritized) +CREATE INDEX idx_document_attachments_integrity_check + ON public.document_attachments (last_integrity_check_at ASC NULLS FIRST) + WHERE is_current_version = true;