-
-
-
Inget underlag bifogat
-
- Enligt bokföringslagen (BFL 5 kap. 6-7 §§) ska varje bokföringspost ha en verifikation som
- underlag. Du kan bifoga underlag nu eller fortsätta utan.
-
-
+
+ Granska uppgifterna innan du bekräftar.
diff --git a/components/dashboard/WelcomeOnboarding.tsx b/components/dashboard/WelcomeOnboarding.tsx
index 75a0f549..5601a5f9 100644
--- a/components/dashboard/WelcomeOnboarding.tsx
+++ b/components/dashboard/WelcomeOnboarding.tsx
@@ -2,7 +2,6 @@
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
-import * as Sentry from '@sentry/nextjs'
import { createClient } from '@/lib/supabase/client'
import { createCompanyFromOnboarding } from '@/lib/company/actions'
import { computeFiscalPeriod } from '@/lib/company/compute-fiscal-period'
@@ -50,10 +49,6 @@ function logError(message: string, extra?: Record
) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: `welcome-onboarding: ${message}`, extra }),
}).catch(() => {})
- Sentry.captureMessage(`welcome-onboarding: ${message}`, {
- level: 'error',
- extra: { ...extra, component: 'welcome-onboarding' },
- })
}
interface WelcomeOnboardingProps {
@@ -209,7 +204,6 @@ export default function WelcomeOnboarding({ firstName, teamId, skipWelcome, hasE
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
logError('create company action threw', { error: message })
- Sentry.captureException(err)
toast({ title: 'Fel', description: 'Ett oväntat fel uppstod. Försök igen.', variant: 'destructive' })
} finally {
setIsSaving(false)
diff --git a/docker/crontab.hosted b/docker/crontab.hosted
index 086b9159..03d824e2 100644
--- a/docker/crontab.hosted
+++ b/docker/crontab.hosted
@@ -2,4 +2,6 @@
0 6 * * * curl -sf -H "Authorization: Bearer ${CRON_SECRET}" ${APP_URL}/api/deadlines/status/cron
0 8 * * * curl -sf -H "Authorization: Bearer ${CRON_SECRET}" ${APP_URL}/api/invoices/reminders/cron
0 0 2 1 * curl -sf -H "Authorization: Bearer ${CRON_SECRET}" ${APP_URL}/api/tax-deadlines/cron
+0 2 * * * curl -sf -H "Authorization: Bearer ${CRON_SECRET}" ${APP_URL}/api/events/cleanup/cron
0 3 * * 0 curl -sf -H "Authorization: Bearer ${CRON_SECRET}" ${APP_URL}/api/documents/verify/cron
+0 4 * * * curl -sf -H "Authorization: Bearer ${CRON_SECRET}" ${APP_URL}/api/sandbox/cleanup/cron
diff --git a/docker/crontab.self-hosted b/docker/crontab.self-hosted
index ec969101..5c4f743f 100644
--- a/docker/crontab.self-hosted
+++ b/docker/crontab.self-hosted
@@ -1,4 +1,6 @@
0 6 * * * curl -sf -H "Authorization: Bearer ${CRON_SECRET}" ${APP_URL}/api/deadlines/status/cron
0 8 * * * curl -sf -H "Authorization: Bearer ${CRON_SECRET}" ${APP_URL}/api/invoices/reminders/cron
0 0 2 1 * curl -sf -H "Authorization: Bearer ${CRON_SECRET}" ${APP_URL}/api/tax-deadlines/cron
+0 2 * * * curl -sf -H "Authorization: Bearer ${CRON_SECRET}" ${APP_URL}/api/events/cleanup/cron
0 3 * * 0 curl -sf -H "Authorization: Bearer ${CRON_SECRET}" ${APP_URL}/api/documents/verify/cron
+0 4 * * * curl -sf -H "Authorization: Bearer ${CRON_SECRET}" ${APP_URL}/api/sandbox/cleanup/cron
diff --git a/docker/extensions.hosted.json b/docker/extensions.hosted.json
index 60c6cca4..26879b79 100644
--- a/docker/extensions.hosted.json
+++ b/docker/extensions.hosted.json
@@ -1 +1 @@
-{"extensions": ["enable-banking", "ai-categorization", "email"]}
+{"extensions": ["enable-banking", "email", "arcim-migration", "tic", "mcp-server"]}
diff --git a/docker/extensions.self-hosted.json b/docker/extensions.self-hosted.json
index 75e40a89..e8087256 100644
--- a/docker/extensions.self-hosted.json
+++ b/docker/extensions.self-hosted.json
@@ -1 +1 @@
-{"extensions": ["ai-categorization", "ai-chat", "receipt-ocr", "invoice-inbox", "email", "push-notifications", "calendar"]}
+{"extensions": ["email", "invoice-inbox", "push-notifications", "calendar", "mcp-server"]}
diff --git a/instrumentation.ts b/instrumentation.ts
index 7cbe93c1..1681b67f 100644
--- a/instrumentation.ts
+++ b/instrumentation.ts
@@ -1,13 +1,4 @@
-import * as Sentry from "@sentry/nextjs";
-
export async function register() {
- if (process.env.NEXT_RUNTIME === "nodejs") {
- await import("./sentry.server.config");
- }
-
- if (process.env.NEXT_RUNTIME === "edge") {
- await import("./sentry.edge.config");
- }
+ // Instrumentation hook — currently a no-op.
+ // Add runtime-specific setup here if needed.
}
-
-export const onRequestError = Sentry.captureRequestError;
diff --git a/lib/auth/__tests__/api-keys.test.ts b/lib/auth/__tests__/api-keys.test.ts
new file mode 100644
index 00000000..2e294780
--- /dev/null
+++ b/lib/auth/__tests__/api-keys.test.ts
@@ -0,0 +1,237 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+
+vi.mock('@supabase/supabase-js', () => ({
+ createClient: vi.fn(),
+}))
+
+import {
+ generateApiKey,
+ hashApiKey,
+ extractBearerToken,
+ validateScopes,
+ hasScope,
+ validateApiKey,
+ DEFAULT_SCOPES,
+} from '../api-keys'
+import { createClient } from '@supabase/supabase-js'
+
+const mockCreateClient = vi.mocked(createClient)
+
+beforeEach(() => {
+ vi.clearAllMocks()
+})
+
+// ============================================================
+// generateApiKey
+// ============================================================
+
+describe('generateApiKey', () => {
+ it('returns key starting with "gnubok_sk_"', () => {
+ const { key } = generateApiKey()
+ expect(key.startsWith('gnubok_sk_')).toBe(true)
+ })
+
+ it('returns 64-char hex SHA-256 hash', () => {
+ const { hash } = generateApiKey()
+ expect(hash).toMatch(/^[0-9a-f]{64}$/)
+ })
+
+ it('returns prefix of KEY_PREFIX + 8 chars', () => {
+ const { key, prefix } = generateApiKey()
+ expect(prefix).toBe(key.slice(0, 'gnubok_sk_'.length + 8))
+ })
+
+ it('generates unique keys on successive calls', () => {
+ const a = generateApiKey()
+ const b = generateApiKey()
+ expect(a.key).not.toBe(b.key)
+ expect(a.hash).not.toBe(b.hash)
+ })
+
+ it('hash matches hashApiKey(key)', () => {
+ const { key, hash } = generateApiKey()
+ expect(hashApiKey(key)).toBe(hash)
+ })
+})
+
+// ============================================================
+// hashApiKey
+// ============================================================
+
+describe('hashApiKey', () => {
+ it('returns 64-char hex string', () => {
+ const hash = hashApiKey('gnubok_sk_test-key')
+ expect(hash).toMatch(/^[0-9a-f]{64}$/)
+ })
+
+ it('is deterministic for same input', () => {
+ const hash1 = hashApiKey('gnubok_sk_deterministic')
+ const hash2 = hashApiKey('gnubok_sk_deterministic')
+ expect(hash1).toBe(hash2)
+ })
+
+ it('produces different hashes for different inputs', () => {
+ const hash1 = hashApiKey('gnubok_sk_key-a')
+ const hash2 = hashApiKey('gnubok_sk_key-b')
+ expect(hash1).not.toBe(hash2)
+ })
+})
+
+// ============================================================
+// extractBearerToken
+// ============================================================
+
+describe('extractBearerToken', () => {
+ it('extracts token from valid Bearer header', () => {
+ const request = new Request('http://localhost', {
+ headers: { authorization: 'Bearer my-secret-token' },
+ })
+ expect(extractBearerToken(request)).toBe('my-secret-token')
+ })
+
+ it('returns null when no authorization header', () => {
+ const request = new Request('http://localhost')
+ expect(extractBearerToken(request)).toBeNull()
+ })
+
+ it('returns null when header is not Bearer scheme', () => {
+ const request = new Request('http://localhost', {
+ headers: { authorization: 'Basic dXNlcjpwYXNz' },
+ })
+ expect(extractBearerToken(request)).toBeNull()
+ })
+
+ it('handles token with special characters', () => {
+ const request = new Request('http://localhost', {
+ headers: { authorization: 'Bearer gnubok_sk_abc+def/ghi=jkl' },
+ })
+ expect(extractBearerToken(request)).toBe('gnubok_sk_abc+def/ghi=jkl')
+ })
+})
+
+// ============================================================
+// validateScopes
+// ============================================================
+
+describe('validateScopes', () => {
+ it('returns null for null input', () => {
+ expect(validateScopes(null)).toBeNull()
+ })
+
+ it('returns null for undefined input', () => {
+ expect(validateScopes(undefined)).toBeNull()
+ })
+
+ it('returns null for non-array input', () => {
+ expect(validateScopes('transactions:read')).toBeNull()
+ expect(validateScopes(42)).toBeNull()
+ expect(validateScopes({ scope: 'transactions:read' })).toBeNull()
+ })
+
+ it('filters to only valid API_KEY_SCOPES', () => {
+ const result = validateScopes(['transactions:read', 'invalid:scope', 'reports:read'])
+ expect(result).toEqual(['transactions:read', 'reports:read'])
+ })
+
+ it('returns null when no valid scopes remain after filter', () => {
+ expect(validateScopes(['invalid:scope', 'also:invalid'])).toBeNull()
+ })
+
+ it('preserves valid scopes from mixed input', () => {
+ const result = validateScopes(['customers:write', 'bogus', 'invoices:read'])
+ expect(result).toEqual(['customers:write', 'invoices:read'])
+ })
+})
+
+// ============================================================
+// hasScope
+// ============================================================
+
+describe('hasScope', () => {
+ it('returns true when scope present in array', () => {
+ expect(hasScope(['transactions:read', 'reports:read'], 'transactions:read')).toBe(true)
+ })
+
+ it('returns false when scope absent', () => {
+ expect(hasScope(['transactions:read', 'reports:read'], 'invoices:write')).toBe(false)
+ })
+})
+
+// ============================================================
+// validateApiKey
+// ============================================================
+
+describe('validateApiKey', () => {
+ function setupMockRpc(response: { data: unknown; error: unknown }) {
+ const mockRpc = vi.fn().mockResolvedValue(response)
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ mockCreateClient.mockReturnValue({ rpc: mockRpc } as any)
+ }
+
+ it('rejects keys not starting with "gnubok_sk_"', async () => {
+ const result = await validateApiKey('invalid-key-format')
+ expect(result).toEqual({ error: 'Invalid API key format', status: 401 })
+ })
+
+ it('rejects when RPC returns error', async () => {
+ setupMockRpc({ data: null, error: { message: 'db error' } })
+
+ const result = await validateApiKey('gnubok_sk_test-key-value')
+ expect(result).toEqual({ error: 'Invalid API key', status: 401 })
+ })
+
+ it('rejects when RPC returns empty data array', async () => {
+ setupMockRpc({ data: [], error: null })
+
+ const result = await validateApiKey('gnubok_sk_test-key-value')
+ expect(result).toEqual({ error: 'Invalid API key', status: 401 })
+ })
+
+ it('returns rate limit error when rate_limited is true', async () => {
+ setupMockRpc({
+ data: [{ user_id: 'u1', company_id: 'c1', scopes: null, rate_limited: true }],
+ error: null,
+ })
+
+ const result = await validateApiKey('gnubok_sk_test-key-value')
+ expect(result).toEqual({ error: 'Rate limit exceeded', status: 429 })
+ })
+
+ it('returns userId, companyId, scopes on success', async () => {
+ setupMockRpc({
+ data: [{
+ user_id: 'user-123',
+ company_id: 'company-456',
+ scopes: ['transactions:read', 'reports:read'],
+ rate_limited: false,
+ }],
+ error: null,
+ })
+
+ const result = await validateApiKey('gnubok_sk_test-key-value')
+ expect(result).toEqual({
+ userId: 'user-123',
+ companyId: 'company-456',
+ scopes: ['transactions:read', 'reports:read'],
+ })
+ })
+
+ it('falls back to DEFAULT_SCOPES when row.scopes is null', async () => {
+ setupMockRpc({
+ data: [{
+ user_id: 'user-123',
+ company_id: 'company-456',
+ scopes: null,
+ rate_limited: false,
+ }],
+ error: null,
+ })
+
+ const result = await validateApiKey('gnubok_sk_test-key-value')
+ expect(result).toEqual({
+ userId: 'user-123',
+ companyId: 'company-456',
+ scopes: DEFAULT_SCOPES,
+ })
+ })
+})
diff --git a/lib/auth/__tests__/invite-tokens.test.ts b/lib/auth/__tests__/invite-tokens.test.ts
new file mode 100644
index 00000000..7f038a1e
--- /dev/null
+++ b/lib/auth/__tests__/invite-tokens.test.ts
@@ -0,0 +1,54 @@
+import { describe, it, expect, vi, afterEach } from 'vitest'
+import { generateInviteToken, hashInviteToken, getInviteExpiry } from '../invite-tokens'
+
+describe('generateInviteToken', () => {
+ it('returns token starting with "gnubok_inv_"', () => {
+ const { token } = generateInviteToken()
+ expect(token.startsWith('gnubok_inv_')).toBe(true)
+ })
+
+ it('returns a 64-char hex SHA-256 hash', () => {
+ const { hash } = generateInviteToken()
+ expect(hash).toMatch(/^[0-9a-f]{64}$/)
+ })
+
+ it('hash matches hashInviteToken(token)', () => {
+ const { token, hash } = generateInviteToken()
+ expect(hashInviteToken(token)).toBe(hash)
+ })
+
+ it('generates unique tokens on successive calls', () => {
+ const a = generateInviteToken()
+ const b = generateInviteToken()
+ expect(a.token).not.toBe(b.token)
+ expect(a.hash).not.toBe(b.hash)
+ })
+})
+
+describe('hashInviteToken', () => {
+ it('returns 64-char hex string', () => {
+ const hash = hashInviteToken('gnubok_inv_test-token')
+ expect(hash).toMatch(/^[0-9a-f]{64}$/)
+ })
+
+ it('is deterministic for same input', () => {
+ const hash1 = hashInviteToken('gnubok_inv_deterministic')
+ const hash2 = hashInviteToken('gnubok_inv_deterministic')
+ expect(hash1).toBe(hash2)
+ })
+})
+
+describe('getInviteExpiry', () => {
+ afterEach(() => {
+ vi.useRealTimers()
+ })
+
+ it('returns a Date exactly 7 days in the future', () => {
+ vi.useFakeTimers()
+ vi.setSystemTime(new Date('2026-04-14T12:00:00Z'))
+
+ const expiry = getInviteExpiry()
+
+ expect(expiry.toISOString()).toBe('2026-04-21T12:00:00.000Z')
+ })
+})
diff --git a/lib/auth/__tests__/oauth-codes.test.ts b/lib/auth/__tests__/oauth-codes.test.ts
new file mode 100644
index 00000000..0dcd7b95
--- /dev/null
+++ b/lib/auth/__tests__/oauth-codes.test.ts
@@ -0,0 +1,135 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
+import crypto from 'crypto'
+import { createAuthCode, decryptAuthCode, verifyPkce, hashAuthCode } from '../oauth-codes'
+
+beforeEach(() => {
+ vi.stubEnv('SUPABASE_SERVICE_ROLE_KEY', 'test-secret-for-oauth-tests')
+})
+
+afterEach(() => {
+ vi.unstubAllEnvs()
+ vi.useRealTimers()
+})
+
+// ============================================================
+// createAuthCode + decryptAuthCode round-trip
+// ============================================================
+
+describe('createAuthCode + decryptAuthCode round-trip', () => {
+ it('encrypts and decrypts preserving userId, codeChallenge, redirectUri', () => {
+ const payload = {
+ userId: 'user-123',
+ codeChallenge: 'challenge-abc',
+ redirectUri: 'https://claude.ai/api/callback',
+ }
+
+ const code = createAuthCode(payload)
+ const decrypted = decryptAuthCode(code)
+
+ expect(decrypted).not.toBeNull()
+ expect(decrypted!.userId).toBe('user-123')
+ expect(decrypted!.codeChallenge).toBe('challenge-abc')
+ expect(decrypted!.redirectUri).toBe('https://claude.ai/api/callback')
+ })
+
+ it('sets exp approximately 5 minutes in future', () => {
+ vi.useFakeTimers()
+ vi.setSystemTime(new Date('2026-04-14T12:00:00Z'))
+
+ const code = createAuthCode({
+ userId: 'user-1',
+ codeChallenge: 'ch',
+ redirectUri: 'http://localhost',
+ })
+ const decrypted = decryptAuthCode(code)
+
+ expect(decrypted).not.toBeNull()
+ // exp should be Date.now() + 5 * 60 * 1000
+ const expectedExp = new Date('2026-04-14T12:00:00Z').getTime() + 5 * 60 * 1000
+ expect(decrypted!.exp).toBe(expectedExp)
+ })
+
+ it('returns null for expired code', () => {
+ vi.useFakeTimers()
+ vi.setSystemTime(new Date('2026-04-14T12:00:00Z'))
+
+ const code = createAuthCode({
+ userId: 'user-1',
+ codeChallenge: 'ch',
+ redirectUri: 'http://localhost',
+ })
+
+ // Advance past 5 minute TTL
+ vi.advanceTimersByTime(5 * 60 * 1000 + 1)
+
+ const decrypted = decryptAuthCode(code)
+ expect(decrypted).toBeNull()
+ })
+
+ it('returns null for tampered ciphertext', () => {
+ const code = createAuthCode({
+ userId: 'user-1',
+ codeChallenge: 'ch',
+ redirectUri: 'http://localhost',
+ })
+
+ // Flip a character in the middle of the encrypted string
+ const chars = code.split('')
+ const mid = Math.floor(chars.length / 2)
+ chars[mid] = chars[mid] === 'A' ? 'B' : 'A'
+ const tampered = chars.join('')
+
+ expect(decryptAuthCode(tampered)).toBeNull()
+ })
+
+ it('returns null for completely invalid base64url', () => {
+ expect(decryptAuthCode('not-a-valid-code!!!')).toBeNull()
+ })
+
+ it('throws when SUPABASE_SERVICE_ROLE_KEY is not set', () => {
+ vi.stubEnv('SUPABASE_SERVICE_ROLE_KEY', '')
+
+ expect(() =>
+ createAuthCode({
+ userId: 'user-1',
+ codeChallenge: 'ch',
+ redirectUri: 'http://localhost',
+ })
+ ).toThrow('SUPABASE_SERVICE_ROLE_KEY is required')
+ })
+})
+
+// ============================================================
+// verifyPkce
+// ============================================================
+
+describe('verifyPkce', () => {
+ it('returns true when SHA256(verifier) matches challenge', () => {
+ const verifier = 'dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
+ // Compute expected challenge using base64url(SHA-256(verifier))
+ const challenge = crypto.createHash('sha256').update(verifier).digest('base64url')
+
+ expect(verifyPkce(verifier, challenge)).toBe(true)
+ })
+
+ it('returns false when verifier does not match challenge', () => {
+ expect(verifyPkce('correct-verifier', 'wrong-challenge')).toBe(false)
+ })
+})
+
+// ============================================================
+// hashAuthCode
+// ============================================================
+
+describe('hashAuthCode', () => {
+ it('returns 64-char hex string', () => {
+ const hash = hashAuthCode('some-auth-code')
+ expect(hash).toMatch(/^[0-9a-f]{64}$/)
+ })
+
+ it('is deterministic for same input', () => {
+ const hash1 = hashAuthCode('deterministic-code')
+ const hash2 = hashAuthCode('deterministic-code')
+ expect(hash1).toBe(hash2)
+ })
+})
diff --git a/lib/init.ts b/lib/init.ts
index 52786719..7e52f57d 100644
--- a/lib/init.ts
+++ b/lib/init.ts
@@ -25,7 +25,6 @@ const REQUIRED_EXTENSION_VARS = [
] as const
const OPTIONAL_VARS = [
- 'SENTRY_DSN',
'LANGFUSE_SECRET_KEY',
'LANGFUSE_PUBLIC_KEY',
] as const
diff --git a/lib/invoices/__tests__/invoice-matching.test.ts b/lib/invoices/__tests__/invoice-matching.test.ts
new file mode 100644
index 00000000..6ffc1e2e
--- /dev/null
+++ b/lib/invoices/__tests__/invoice-matching.test.ts
@@ -0,0 +1,355 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import {
+ amountsMatchExact,
+ amountsMatchFuzzy,
+ customerNameMatches,
+ calculateMatchScore,
+ findMatchingInvoices,
+ getBestInvoiceMatch,
+} from '../invoice-matching'
+import type { Transaction, Invoice, Customer } from '@/types'
+import { makeTransaction, makeInvoice, makeCustomer, createMockSupabase } from '@/tests/helpers'
+
+// ============================================================
+// amountsMatchExact
+// ============================================================
+
+describe('amountsMatchExact', () => {
+ it('matches identical amounts', () => {
+ expect(amountsMatchExact(1000, 1000)).toBe(true)
+ })
+
+ it('matches amounts differing only in floating-point noise', () => {
+ // 1000.004 rounds to 1000.00, same as 1000.00
+ expect(amountsMatchExact(1000.004, 1000)).toBe(true)
+ })
+
+ it('rejects amounts differing by 0.01', () => {
+ expect(amountsMatchExact(1000.01, 1000)).toBe(false)
+ })
+})
+
+// ============================================================
+// amountsMatchFuzzy
+// ============================================================
+
+describe('amountsMatchFuzzy', () => {
+ it('matches amounts within 1% tolerance', () => {
+ // 990 vs 1000 → diff=10, tolerance=min(10,500)=10 → 10 <= 10
+ expect(amountsMatchFuzzy(990, 1000)).toBe(true)
+ })
+
+ it('rejects amounts outside 1% tolerance', () => {
+ // 980 vs 1000 → diff=20, tolerance=min(10,500)=10 → 20 > 10
+ expect(amountsMatchFuzzy(980, 1000)).toBe(false)
+ })
+
+ it('returns false when invoiceTotal is 0', () => {
+ expect(amountsMatchFuzzy(100, 0)).toBe(false)
+ })
+
+ it('caps tolerance at 500 SEK for large invoices', () => {
+ // 100000 vs 100600 → diff=600, tolerance=min(100000*0.01=1000, 500)=500 → 600 > 500
+ expect(amountsMatchFuzzy(100600, 100000)).toBe(false)
+ // 100000 vs 100400 → diff=400, tolerance=500 → 400 <= 500
+ expect(amountsMatchFuzzy(100400, 100000)).toBe(true)
+ })
+})
+
+// ============================================================
+// customerNameMatches
+// ============================================================
+
+describe('customerNameMatches', () => {
+ it('matches when significant word from customer name appears in description', () => {
+ expect(customerNameMatches('Kontorsbolaget AB', 'Betalning Kontorsbolaget', null)).toBe(true)
+ })
+
+ it('ignores words shorter than 3 characters', () => {
+ // "AB" is 2 chars, filtered out
+ expect(customerNameMatches('AB', 'AB payment', null)).toBe(false)
+ })
+
+ it('matches against merchant_name', () => {
+ expect(customerNameMatches('Kontorsbolaget', 'Random description', 'Kontorsbolaget AB')).toBe(true)
+ })
+
+ it('returns false when customerName is undefined', () => {
+ expect(customerNameMatches(undefined as unknown as string, 'Description', null)).toBe(false)
+ })
+
+ it('is case-insensitive', () => {
+ expect(customerNameMatches('KONTORSBOLAGET', 'betalning kontorsbolaget', null)).toBe(true)
+ })
+})
+
+// ============================================================
+// calculateMatchScore
+// ============================================================
+
+describe('calculateMatchScore', () => {
+ function makeTx(overrides: Partial = {}): Transaction {
+ return makeTransaction({ amount: 12500, description: 'Betalning Kundnamn AB', merchant_name: null, ...overrides })
+ }
+
+ function makeInv(overrides: Partial = {}): Invoice & { customer?: Customer } {
+ return {
+ ...makeInvoice({ total: 12500 }),
+ customer: makeCustomer({ name: 'Kundnamn AB' }),
+ ...overrides,
+ }
+ }
+
+ it('returns 0.95 for exact amount + customer name match', () => {
+ const { confidence } = calculateMatchScore(makeTx(), makeInv())
+ expect(confidence).toBe(0.95)
+ })
+
+ it('returns 0.80 for exact amount without customer match', () => {
+ const { confidence } = calculateMatchScore(
+ makeTx({ description: 'Random payment', merchant_name: null }),
+ makeInv({ customer: makeCustomer({ name: 'Completely Different Co' }) })
+ )
+ expect(confidence).toBe(0.80)
+ })
+
+ it('returns 0.70 for fuzzy amount + customer name match', () => {
+ // 12375 is within 1% of 12500 (diff=125, tolerance=min(125,500)=125)
+ const { confidence } = calculateMatchScore(
+ makeTx({ amount: 12375 }),
+ makeInv()
+ )
+ expect(confidence).toBe(0.70)
+ })
+
+ it('returns 0.50 for fuzzy amount without customer match', () => {
+ const { confidence } = calculateMatchScore(
+ makeTx({ amount: 12375, description: 'Random', merchant_name: null }),
+ makeInv({ customer: makeCustomer({ name: 'Completely Different Co' }) })
+ )
+ expect(confidence).toBe(0.50)
+ })
+
+ it('returns 0 confidence when no amount match', () => {
+ const { confidence } = calculateMatchScore(
+ makeTx({ amount: 99999 }),
+ makeInv()
+ )
+ expect(confidence).toBe(0)
+ })
+})
+
+// ============================================================
+// findMatchingInvoices (integration — mock Supabase)
+// ============================================================
+
+describe('findMatchingInvoices', () => {
+ const { supabase, mockResult } = createMockSupabase()
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('returns empty for expense transactions (amount <= 0)', async () => {
+ const tx = makeTransaction({ amount: -1000 })
+ const result = await findMatchingInvoices(supabase as never, 'company-1', tx)
+ expect(result).toEqual([])
+ })
+
+ it('returns empty when Supabase query errors', async () => {
+ mockResult({ data: null, error: { message: 'db error' } })
+ const tx = makeTransaction({ amount: 12500 })
+ const result = await findMatchingInvoices(supabase as never, 'company-1', tx)
+ expect(result).toEqual([])
+ })
+
+ it('matches by OCR reference with confidence 0.99', async () => {
+ const tx = makeTransaction({ amount: 12500, reference: 'F-2024001' })
+ mockResult({
+ data: [
+ { ...makeInvoice({ invoice_number: 'F-2024001', total: 12500, status: 'sent', remaining_amount: 12500, currency: 'SEK' }) },
+ ],
+ error: null,
+ })
+
+ const result = await findMatchingInvoices(supabase as never, 'company-1', tx)
+ expect(result).toHaveLength(1)
+ expect(result[0].confidence).toBe(0.99)
+ expect(result[0].matchReason).toContain('OCR-referens')
+ })
+
+ it('returns immediately on OCR match without further scoring', async () => {
+ const tx = makeTransaction({ amount: 12500, reference: 'F-2024001' })
+ mockResult({
+ data: [
+ { ...makeInvoice({ invoice_number: 'F-2024001', total: 12500, status: 'sent', remaining_amount: 12500, currency: 'SEK' }) },
+ // Second invoice with exact amount — should not be scored
+ {
+ ...makeInvoice({ id: 'inv-2', invoice_number: 'F-2024002', total: 12500, status: 'sent', remaining_amount: 12500, currency: 'SEK' }),
+ customer: makeCustomer({ name: 'Test match description' }),
+ },
+ ],
+ error: null,
+ })
+
+ const result = await findMatchingInvoices(supabase as never, 'company-1', tx)
+ // Only the OCR match should be returned
+ expect(result).toHaveLength(1)
+ expect(result[0].confidence).toBe(0.99)
+ })
+
+ it('scores exact amount + customer name at 0.95', async () => {
+ const tx = makeTransaction({ amount: 12500, description: 'Betalning Testbolaget', reference: null })
+ mockResult({
+ data: [{
+ ...makeInvoice({ total: 12500, status: 'sent', remaining_amount: 12500, currency: 'SEK' }),
+ customer: makeCustomer({ name: 'Testbolaget AB' }),
+ }],
+ error: null,
+ })
+
+ const result = await findMatchingInvoices(supabase as never, 'company-1', tx)
+ expect(result).toHaveLength(1)
+ expect(result[0].confidence).toBe(0.95)
+ })
+
+ it('scores exact amount only at 0.80', async () => {
+ const tx = makeTransaction({ amount: 12500, description: 'Unrelated text', merchant_name: null, reference: null })
+ mockResult({
+ data: [{
+ ...makeInvoice({ total: 12500, status: 'sent', remaining_amount: 12500, currency: 'SEK' }),
+ customer: makeCustomer({ name: 'Completely Different Name' }),
+ }],
+ error: null,
+ })
+
+ const result = await findMatchingInvoices(supabase as never, 'company-1', tx)
+ expect(result).toHaveLength(1)
+ expect(result[0].confidence).toBe(0.80)
+ })
+
+ it('sorts matches by confidence descending', async () => {
+ const tx = makeTransaction({ amount: 12500, description: 'Betalning Testbolaget', merchant_name: null, reference: null })
+ mockResult({
+ data: [
+ // Exact amount, no name match → 0.80
+ {
+ ...makeInvoice({ id: 'inv-low', total: 12500, status: 'sent', remaining_amount: 12500, currency: 'SEK' }),
+ customer: makeCustomer({ name: 'Nope Corp' }),
+ },
+ // Exact amount + name match → 0.95
+ {
+ ...makeInvoice({ id: 'inv-high', total: 12500, status: 'sent', remaining_amount: 12500, currency: 'SEK' }),
+ customer: makeCustomer({ name: 'Testbolaget AB' }),
+ },
+ ],
+ error: null,
+ })
+
+ const result = await findMatchingInvoices(supabase as never, 'company-1', tx)
+ expect(result).toHaveLength(2)
+ expect(result[0].confidence).toBe(0.95)
+ expect(result[1].confidence).toBe(0.80)
+ })
+
+ it('filters out matches below 0.50 threshold', async () => {
+ const tx = makeTransaction({ amount: 99999, description: 'No match', merchant_name: null, reference: null })
+ mockResult({
+ data: [{
+ ...makeInvoice({ total: 50000, status: 'sent', remaining_amount: 50000, currency: 'SEK' }),
+ customer: makeCustomer({ name: 'Irrelevant' }),
+ }],
+ error: null,
+ })
+
+ const result = await findMatchingInvoices(supabase as never, 'company-1', tx)
+ expect(result).toEqual([])
+ })
+
+ it('uses remaining_amount for partially_paid invoices', async () => {
+ const tx = makeTransaction({ amount: 5000, description: 'Unrelated', merchant_name: null, reference: null })
+ mockResult({
+ data: [{
+ ...makeInvoice({ total: 12500, remaining_amount: 5000, status: 'partially_paid', currency: 'SEK' }),
+ customer: makeCustomer({ name: 'Different' }),
+ }],
+ error: null,
+ })
+
+ const result = await findMatchingInvoices(supabase as never, 'company-1', tx)
+ expect(result).toHaveLength(1)
+ expect(result[0].confidence).toBe(0.80) // exact amount match
+ })
+
+ it('skips invoices with non-matching currency', async () => {
+ const tx = makeTransaction({ amount: 12500, currency: 'SEK', description: 'Payment', merchant_name: null, reference: null })
+ mockResult({
+ data: [{
+ ...makeInvoice({ total: 12500, remaining_amount: 12500, status: 'sent', currency: 'EUR', total_sek: null }),
+ customer: makeCustomer({ name: 'Different' }),
+ }],
+ error: null,
+ })
+
+ const result = await findMatchingInvoices(supabase as never, 'company-1', tx)
+ // EUR invoice with no total_sek → currency mismatch → skipped
+ expect(result).toEqual([])
+ })
+})
+
+// ============================================================
+// getBestInvoiceMatch
+// ============================================================
+
+describe('getBestInvoiceMatch', () => {
+ const { supabase, mockResult } = createMockSupabase()
+
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('returns highest-confidence match when above minConfidence', async () => {
+ const tx = makeTransaction({ amount: 12500, description: 'Unrelated', merchant_name: null, reference: null })
+ mockResult({
+ data: [{
+ ...makeInvoice({ total: 12500, status: 'sent', remaining_amount: 12500, currency: 'SEK' }),
+ customer: makeCustomer({ name: 'Different' }),
+ }],
+ error: null,
+ })
+
+ const result = await getBestInvoiceMatch(supabase as never, 'company-1', tx)
+ expect(result).not.toBeNull()
+ expect(result!.confidence).toBe(0.80)
+ })
+
+ it('returns null when best match below minConfidence', async () => {
+ const tx = makeTransaction({ amount: 12500, description: 'Unrelated', merchant_name: null, reference: null })
+ mockResult({
+ data: [{
+ ...makeInvoice({ total: 12500, status: 'sent', remaining_amount: 12500, currency: 'SEK' }),
+ customer: makeCustomer({ name: 'Different' }),
+ }],
+ error: null,
+ })
+
+ // minConfidence 0.90 → 0.80 match rejected
+ const result = await getBestInvoiceMatch(supabase as never, 'company-1', tx, 0.90)
+ expect(result).toBeNull()
+ })
+
+ it('defaults minConfidence to 0.80', async () => {
+ const tx = makeTransaction({ amount: 12500, description: 'Unrelated', merchant_name: null, reference: null })
+ mockResult({
+ data: [{
+ ...makeInvoice({ total: 12500, status: 'sent', remaining_amount: 12500, currency: 'SEK' }),
+ customer: makeCustomer({ name: 'Different' }),
+ }],
+ error: null,
+ })
+
+ // Exact amount only → 0.80, meets default threshold
+ const result = await getBestInvoiceMatch(supabase as never, 'company-1', tx)
+ expect(result).not.toBeNull()
+ })
+})
diff --git a/lib/invoices/__tests__/vat-rules.test.ts b/lib/invoices/__tests__/vat-rules.test.ts
new file mode 100644
index 00000000..8a080fe9
--- /dev/null
+++ b/lib/invoices/__tests__/vat-rules.test.ts
@@ -0,0 +1,281 @@
+import { describe, it, expect } from 'vitest'
+import {
+ getAvailableVatRates,
+ getVatTreatmentForRate,
+ getVatRules,
+ calculateVat,
+ calculateTotal,
+ formatVatRate,
+ getVatTreatmentLabel,
+ getVatSummaryFromItems,
+ getMomsRutaDescription,
+} from '../vat-rules'
+
+// ============================================================
+// getAvailableVatRates
+// ============================================================
+
+describe('getAvailableVatRates', () => {
+ it('returns all 4 Swedish rates for individual customer', () => {
+ const rates = getAvailableVatRates('individual')
+ expect(rates).toHaveLength(4)
+ expect(rates.map((r) => r.rate)).toEqual([25, 12, 6, 0])
+ expect(rates.map((r) => r.treatment)).toEqual([
+ 'standard_25',
+ 'reduced_12',
+ 'reduced_6',
+ 'exempt',
+ ])
+ })
+
+ it('returns all 4 Swedish rates for swedish_business', () => {
+ const rates = getAvailableVatRates('swedish_business')
+ expect(rates).toHaveLength(4)
+ expect(rates.map((r) => r.rate)).toEqual([25, 12, 6, 0])
+ })
+
+ it('returns only reverse_charge 0% for eu_business with validated VAT', () => {
+ const rates = getAvailableVatRates('eu_business', true)
+ expect(rates).toHaveLength(1)
+ expect(rates[0]).toEqual({
+ rate: 0,
+ label: '0% (omvänd skattskyldighet)',
+ treatment: 'reverse_charge',
+ })
+ })
+
+ it('returns all 4 rates for eu_business WITHOUT validated VAT', () => {
+ // ML compliance: must charge Swedish VAT when VAT number not validated
+ const rates = getAvailableVatRates('eu_business', false)
+ expect(rates).toHaveLength(4)
+ expect(rates.map((r) => r.rate)).toEqual([25, 12, 6, 0])
+ })
+
+ it('returns only export 0% for non_eu_business', () => {
+ const rates = getAvailableVatRates('non_eu_business')
+ expect(rates).toHaveLength(1)
+ expect(rates[0]).toEqual({
+ rate: 0,
+ label: '0% (export)',
+ treatment: 'export',
+ })
+ })
+
+ it('defaults vatNumberValidated to false', () => {
+ // eu_business without explicit vatNumberValidated should get all rates
+ const rates = getAvailableVatRates('eu_business')
+ expect(rates).toHaveLength(4)
+ })
+})
+
+// ============================================================
+// getVatTreatmentForRate
+// ============================================================
+
+describe('getVatTreatmentForRate', () => {
+ it('maps 25 → standard_25', () => {
+ expect(getVatTreatmentForRate(25)).toBe('standard_25')
+ })
+
+ it('maps 12 → reduced_12', () => {
+ expect(getVatTreatmentForRate(12)).toBe('reduced_12')
+ })
+
+ it('maps 6 → reduced_6', () => {
+ expect(getVatTreatmentForRate(6)).toBe('reduced_6')
+ })
+
+ it('maps 0 → exempt', () => {
+ expect(getVatTreatmentForRate(0)).toBe('exempt')
+ })
+
+ it('defaults unknown rates to standard_25', () => {
+ expect(getVatTreatmentForRate(15)).toBe('standard_25')
+ expect(getVatTreatmentForRate(99)).toBe('standard_25')
+ })
+})
+
+// ============================================================
+// getVatRules
+// ============================================================
+
+describe('getVatRules', () => {
+ it('returns standard_25 / rate 25 / ruta 05 for individual', () => {
+ const rules = getVatRules('individual')
+ expect(rules).toEqual({
+ treatment: 'standard_25',
+ rate: 25,
+ momsRuta: '05',
+ })
+ })
+
+ it('returns standard_25 / rate 25 / ruta 05 for swedish_business', () => {
+ const rules = getVatRules('swedish_business')
+ expect(rules).toEqual({
+ treatment: 'standard_25',
+ rate: 25,
+ momsRuta: '05',
+ })
+ })
+
+ it('returns reverse_charge / rate 0 / ruta 39 for eu_business with validated VAT', () => {
+ const rules = getVatRules('eu_business', true)
+ expect(rules.treatment).toBe('reverse_charge')
+ expect(rules.rate).toBe(0)
+ expect(rules.momsRuta).toBe('39')
+ // Verify text references Article 196 of Council Directive 2006/112/EC
+ expect(rules.reverseChargeText).toContain('Article 196')
+ expect(rules.reverseChargeText).toContain('2006/112/EC')
+ })
+
+ it('returns standard_25 / rate 25 / ruta 05 for eu_business WITHOUT validated VAT', () => {
+ const rules = getVatRules('eu_business', false)
+ expect(rules).toEqual({
+ treatment: 'standard_25',
+ rate: 25,
+ momsRuta: '05',
+ })
+ })
+
+ it('returns export / rate 0 / ruta 40 for non_eu_business', () => {
+ const rules = getVatRules('non_eu_business')
+ expect(rules.treatment).toBe('export')
+ expect(rules.rate).toBe(0)
+ expect(rules.momsRuta).toBe('40')
+ // Verify text references ML 10 kap
+ expect(rules.reverseChargeText).toContain('ML 10 kap')
+ })
+
+ it('defaults to standard_25 for unknown customerType', () => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const rules = getVatRules('unknown_type' as any)
+ expect(rules).toEqual({
+ treatment: 'standard_25',
+ rate: 25,
+ momsRuta: '05',
+ })
+ })
+})
+
+// ============================================================
+// calculateVat
+// Pin: vatRate is a whole number (25, not 0.25).
+// Formula: Math.round(subtotal * vatRate) / 100
+// ============================================================
+
+describe('calculateVat', () => {
+ it('calculates 25% of 10000 → 2500', () => {
+ expect(calculateVat(10000, 25)).toBe(2500)
+ })
+
+ it('calculates 12% of 5000 → 600', () => {
+ expect(calculateVat(5000, 12)).toBe(600)
+ })
+
+ it('calculates 6% of 3000 → 180', () => {
+ expect(calculateVat(3000, 6)).toBe(180)
+ })
+
+ it('calculates 0% of 10000 → 0', () => {
+ expect(calculateVat(10000, 0)).toBe(0)
+ })
+
+ it('rounds correctly: 99.99 at 25% → 25', () => {
+ // Math.round(99.99 * 25) / 100 = Math.round(2499.75) / 100 = 2500 / 100 = 25
+ expect(calculateVat(99.99, 25)).toBe(25)
+ })
+})
+
+// ============================================================
+// calculateTotal
+// ============================================================
+
+describe('calculateTotal', () => {
+ it('returns subtotal + VAT rounded: 10000 at 25% → 12500', () => {
+ expect(calculateTotal(10000, 25)).toBe(12500)
+ })
+
+ it('handles 0% VAT: total equals subtotal', () => {
+ expect(calculateTotal(5000, 0)).toBe(5000)
+ })
+})
+
+// ============================================================
+// formatVatRate
+// ============================================================
+
+describe('formatVatRate', () => {
+ it('formats 25 as "25%"', () => {
+ expect(formatVatRate(25)).toBe('25%')
+ })
+
+ it('formats 0 as "0%"', () => {
+ expect(formatVatRate(0)).toBe('0%')
+ })
+})
+
+// ============================================================
+// getVatTreatmentLabel
+// ============================================================
+
+describe('getVatTreatmentLabel', () => {
+ it('returns correct Swedish label for each treatment', () => {
+ expect(getVatTreatmentLabel('standard_25')).toBe('25% moms')
+ expect(getVatTreatmentLabel('reduced_12')).toBe('12% moms')
+ expect(getVatTreatmentLabel('reduced_6')).toBe('6% moms')
+ expect(getVatTreatmentLabel('reverse_charge')).toBe('Omvänd skattskyldighet (0%)')
+ expect(getVatTreatmentLabel('export')).toBe('Export (0%)')
+ expect(getVatTreatmentLabel('exempt')).toBe('Momsfritt')
+ })
+})
+
+// ============================================================
+// getVatSummaryFromItems
+// ============================================================
+
+describe('getVatSummaryFromItems', () => {
+ it('returns single rate info when all items have same rate', () => {
+ const result = getVatSummaryFromItems([{ vat_rate: 25 }, { vat_rate: 25 }])
+ expect(result.isMixed).toBe(false)
+ expect(result.rate).toBe(25)
+ expect(result.treatment).toBe('standard_25')
+ expect(result.label).toBe('25% moms')
+ })
+
+ it('returns isMixed=true when items have different rates', () => {
+ const result = getVatSummaryFromItems([{ vat_rate: 25 }, { vat_rate: 12 }])
+ expect(result.isMixed).toBe(true)
+ expect(result.rate).toBeNull()
+ expect(result.treatment).toBeNull()
+ expect(result.label).toBe('Blandade momssatser')
+ })
+
+ it('treats null vat_rate as 0', () => {
+ const result = getVatSummaryFromItems([{ vat_rate: null }, { vat_rate: null }])
+ expect(result.isMixed).toBe(false)
+ expect(result.rate).toBe(0)
+ expect(result.treatment).toBe('exempt')
+ })
+})
+
+// ============================================================
+// getMomsRutaDescription
+// ============================================================
+
+describe('getMomsRutaDescription', () => {
+ it('maps ruta 05 → "Utgående moms 25%"', () => {
+ expect(getMomsRutaDescription('05')).toBe('Utgående moms 25%')
+ })
+
+ it('maps ruta 39 → "Försäljning av tjänster till annat EU-land"', () => {
+ expect(getMomsRutaDescription('39')).toBe('Försäljning av tjänster till annat EU-land')
+ })
+
+ it('maps ruta 40 → "Export utanför EU"', () => {
+ expect(getMomsRutaDescription('40')).toBe('Export utanför EU')
+ })
+
+ it('returns the ruta string itself for unknown rutor', () => {
+ expect(getMomsRutaDescription('99')).toBe('99')
+ })
+})
diff --git a/lib/invoices/invoice-matching.ts b/lib/invoices/invoice-matching.ts
index 5d6defb9..cc9bab42 100644
--- a/lib/invoices/invoice-matching.ts
+++ b/lib/invoices/invoice-matching.ts
@@ -27,7 +27,7 @@ const FUZZY_TOLERANCE = 0.01
/**
* Check if two amounts match exactly (within rounding)
*/
-function amountsMatchExact(transactionAmount: number, invoiceTotal: number): boolean {
+export function amountsMatchExact(transactionAmount: number, invoiceTotal: number): boolean {
// Round to 2 decimal places for comparison
const txRounded = Math.round(transactionAmount * 100) / 100
const invRounded = Math.round(invoiceTotal * 100) / 100
@@ -37,7 +37,7 @@ function amountsMatchExact(transactionAmount: number, invoiceTotal: number): boo
/**
* Check if two amounts match within fuzzy tolerance (±1%)
*/
-function amountsMatchFuzzy(transactionAmount: number, invoiceTotal: number): boolean {
+export function amountsMatchFuzzy(transactionAmount: number, invoiceTotal: number): boolean {
if (invoiceTotal === 0) return false
const diff = Math.abs(transactionAmount - invoiceTotal)
// Cap fuzzy tolerance at 500 SEK to prevent false positives on large invoices
@@ -48,7 +48,7 @@ function amountsMatchFuzzy(transactionAmount: number, invoiceTotal: number): boo
/**
* Check if customer name appears in transaction counterparty
*/
-function customerNameMatches(
+export function customerNameMatches(
customerName: string | undefined,
transactionDescription: string,
merchantName: string | null
@@ -65,7 +65,7 @@ function customerNameMatches(
/**
* Calculate confidence score and match reason for an invoice match
*/
-function calculateMatchScore(
+export function calculateMatchScore(
transaction: Transaction,
invoice: Invoice & { customer?: Customer }
): { confidence: number; matchReason: string } {
diff --git a/lib/reports/__tests__/balance-sheet.test.ts b/lib/reports/__tests__/balance-sheet.test.ts
index 3f6efe3e..00c93e37 100644
--- a/lib/reports/__tests__/balance-sheet.test.ts
+++ b/lib/reports/__tests__/balance-sheet.test.ts
@@ -172,6 +172,49 @@ describe('generateBalanceSheet', () => {
expect(report.total_equity_liabilities).toBe(32500)
})
+ it('handles negative asset balance (net credit on class 1 account)', async () => {
+ mockTrialBalance.mockResolvedValue({
+ rows: [
+ makeRow({ account_number: '1930', account_name: 'Bank', account_class: 1, closing_debit: 50000, closing_credit: 0 }),
+ // Receivables with net credit (customer overpayment)
+ makeRow({ account_number: '1510', account_name: 'Kundfordringar', account_class: 1, closing_debit: 0, closing_credit: 5000 }),
+ ],
+ totalDebit: 50000,
+ totalCredit: 5000,
+ isBalanced: false,
+ })
+
+ const report = await generateBalanceSheet(supabase, 'company-1', 'period-1')
+
+ const receivables = report.asset_sections.find(s => s.rows.some(r => r.account_number === '1510'))
+ expect(receivables).toBeDefined()
+ expect(receivables!.rows.find(r => r.account_number === '1510')!.amount).toBe(-5000)
+ expect(report.total_assets).toBe(45000) // 50000 - 5000
+ })
+
+ it('rounding boundary: 0.004 rounds to 0 and is excluded, 0.005 rounds to 0.01 and is included', async () => {
+ // Amounts go through Math.round(x * 100) / 100 before the > 0.005 filter.
+ // Due to IEEE 754, 0.005 * 100 is slightly above 0.5, so Math.round rounds UP to 1,
+ // giving 0.01 which passes > 0.005. Meanwhile 0.004 rounds to 0.
+ mockTrialBalance.mockResolvedValue({
+ rows: [
+ makeRow({ account_number: '1930', account_name: 'Bank', account_class: 1, closing_debit: 1000, closing_credit: 0 }),
+ makeRow({ account_number: '1940', account_name: 'Excluded', account_class: 1, closing_debit: 0.004, closing_credit: 0 }),
+ makeRow({ account_number: '1950', account_name: 'Included', account_class: 1, closing_debit: 0.005, closing_credit: 0 }),
+ ],
+ totalDebit: 1000.009,
+ totalCredit: 0,
+ isBalanced: false,
+ })
+
+ const report = await generateBalanceSheet(supabase, 'company-1', 'period-1')
+
+ const bankSection = report.asset_sections.find(s => s.title === 'Kassa och bank')!
+ // 1930 (1000) and 1950 (0.005 → rounds to 0.01) are included; 1940 (0.004 → rounds to 0) is excluded
+ expect(bankSection.rows).toHaveLength(2)
+ expect(bankSection.rows.map(r => r.account_number)).toEqual(['1930', '1950'])
+ })
+
it('uses Math.round for monetary precision on subtotals', async () => {
mockTrialBalance.mockResolvedValue({
rows: [
diff --git a/lib/reports/__tests__/income-statement.test.ts b/lib/reports/__tests__/income-statement.test.ts
index 4d68369a..509e946a 100644
--- a/lib/reports/__tests__/income-statement.test.ts
+++ b/lib/reports/__tests__/income-statement.test.ts
@@ -171,6 +171,49 @@ describe('generateIncomeStatement', () => {
expect(report.revenue_sections[0].title).toBe('Huvudintäkter')
})
+ it('rounding boundary: 0.004 rounds to 0 and is excluded, 0.005 rounds to 0.01 and is included', async () => {
+ // Amounts go through Math.round(x * 100) / 100 before the > 0.005 filter.
+ // 0.004 → Math.round(0.4) = 0 → excluded. 0.005 → Math.round(0.5+ε) = 1 → 0.01 → included.
+ mockTrialBalance.mockResolvedValue({
+ rows: [
+ makeRow({ account_number: '3001', account_name: 'Revenue', account_class: 3, closing_credit: 10000, closing_debit: 0 }),
+ makeRow({ account_number: '3002', account_name: 'Excluded', account_class: 3, closing_credit: 0.004, closing_debit: 0 }),
+ makeRow({ account_number: '3003', account_name: 'Included', account_class: 3, closing_credit: 0.005, closing_debit: 0 }),
+ ],
+ totalDebit: 0,
+ totalCredit: 10000.009,
+ isBalanced: false,
+ })
+
+ const report = await generateIncomeStatement(supabase, 'company-1', 'period-1')
+
+ const section = report.revenue_sections.find(s => s.title === 'Huvudintäkter')!
+ // 3001 and 3003 included; 3002 excluded
+ expect(section.rows).toHaveLength(2)
+ expect(section.rows.map(r => r.account_number)).toEqual(['3001', '3003'])
+ })
+
+ it('subtotal includes sub-threshold rows that are filtered from visible rows', async () => {
+ mockTrialBalance.mockResolvedValue({
+ rows: [
+ makeRow({ account_number: '3001', account_name: 'Revenue', account_class: 3, closing_credit: 10000, closing_debit: 0 }),
+ // Amount 0.004 — below threshold, filtered from rows but included in subtotal
+ makeRow({ account_number: '3002', account_name: 'Micro', account_class: 3, closing_credit: 0.004, closing_debit: 0 }),
+ ],
+ totalDebit: 0,
+ totalCredit: 10000.004,
+ isBalanced: false,
+ })
+
+ const report = await generateIncomeStatement(supabase, 'company-1', 'period-1')
+
+ const section = report.revenue_sections.find(s => s.title === 'Huvudintäkter')!
+ // Only the 10000 row is visible
+ expect(section.rows).toHaveLength(1)
+ // But subtotal includes both rows (Math.round((10000 + 0.004) * 100) / 100 = 10000)
+ expect(section.subtotal).toBe(10000)
+ })
+
it('ignores class 1-2 accounts (balance sheet)', async () => {
mockTrialBalance.mockResolvedValue({
rows: [
diff --git a/lib/reports/__tests__/opening-balances.test.ts b/lib/reports/__tests__/opening-balances.test.ts
new file mode 100644
index 00000000..a277b583
--- /dev/null
+++ b/lib/reports/__tests__/opening-balances.test.ts
@@ -0,0 +1,132 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+
+vi.mock('@/lib/supabase/fetch-all', () => ({
+ fetchAllRows: vi.fn(),
+}))
+
+import { getOpeningBalances } from '../opening-balances'
+import { fetchAllRows } from '@/lib/supabase/fetch-all'
+
+const mockFetchAllRows = vi.mocked(fetchAllRows)
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const supabase = {} as any
+
+beforeEach(() => {
+ vi.clearAllMocks()
+})
+
+describe('getOpeningBalances', () => {
+ it('returns empty map and null obEntryId when period is null', async () => {
+ const { balances, obEntryId } = await getOpeningBalances(supabase, 'company-1', null)
+
+ expect(balances.size).toBe(0)
+ expect(obEntryId).toBeNull()
+ })
+
+ describe('with opening_balance_entry_id (OB entry path)', () => {
+ const period = {
+ period_start: '2025-01-01',
+ opening_balance_entry_id: 'ob-entry-123',
+ }
+
+ it('returns balances from the OB entry lines', async () => {
+ mockFetchAllRows.mockResolvedValue([
+ { account_number: '1930', debit_amount: 50000, credit_amount: 0 },
+ { account_number: '2440', debit_amount: 0, credit_amount: 10000 },
+ ])
+
+ const { balances, obEntryId } = await getOpeningBalances(supabase, 'company-1', period)
+
+ expect(balances.get('1930')).toEqual({ debit: 50000, credit: 0 })
+ expect(balances.get('2440')).toEqual({ debit: 0, credit: 10000 })
+ expect(obEntryId).toBe('ob-entry-123')
+ })
+
+ it('aggregates multiple lines for the same account', async () => {
+ mockFetchAllRows.mockResolvedValue([
+ { account_number: '1930', debit_amount: 30000, credit_amount: 0 },
+ { account_number: '1930', debit_amount: 20000, credit_amount: 0 },
+ ])
+
+ const { balances } = await getOpeningBalances(supabase, 'company-1', period)
+
+ expect(balances.get('1930')).toEqual({ debit: 50000, credit: 0 })
+ })
+
+ it('returns the obEntryId string', async () => {
+ mockFetchAllRows.mockResolvedValue([])
+
+ const { obEntryId } = await getOpeningBalances(supabase, 'company-1', period)
+
+ expect(obEntryId).toBe('ob-entry-123')
+ })
+ })
+
+ describe('without opening_balance_entry_id (fallback path)', () => {
+ const period = {
+ period_start: '2025-01-01',
+ opening_balance_entry_id: null,
+ }
+
+ it('computes balances from all prior entries', async () => {
+ mockFetchAllRows.mockResolvedValue([
+ { account_number: '1930', debit_amount: 100000, credit_amount: 5000 },
+ { account_number: '3001', debit_amount: 0, credit_amount: 80000 },
+ ])
+
+ const { balances, obEntryId } = await getOpeningBalances(supabase, 'company-1', period)
+
+ expect(balances.get('1930')).toEqual({ debit: 100000, credit: 5000 })
+ expect(balances.get('3001')).toEqual({ debit: 0, credit: 80000 })
+ expect(obEntryId).toBeNull()
+ })
+
+ it('aggregates multiple lines per account', async () => {
+ mockFetchAllRows.mockResolvedValue([
+ { account_number: '1510', debit_amount: 5000, credit_amount: 0 },
+ { account_number: '1510', debit_amount: 3000, credit_amount: 1000 },
+ ])
+
+ const { balances } = await getOpeningBalances(supabase, 'company-1', period)
+
+ expect(balances.get('1510')).toEqual({ debit: 8000, credit: 1000 })
+ })
+
+ it('returns null obEntryId', async () => {
+ mockFetchAllRows.mockResolvedValue([])
+
+ const { obEntryId } = await getOpeningBalances(supabase, 'company-1', period)
+
+ expect(obEntryId).toBeNull()
+ })
+ })
+
+ it('coerces null/undefined debit/credit to 0', async () => {
+ const period = {
+ period_start: '2025-01-01',
+ opening_balance_entry_id: 'ob-entry-1',
+ }
+
+ mockFetchAllRows.mockResolvedValue([
+ { account_number: '1930', debit_amount: null, credit_amount: undefined },
+ ])
+
+ const { balances } = await getOpeningBalances(supabase, 'company-1', period)
+
+ expect(balances.get('1930')).toEqual({ debit: 0, credit: 0 })
+ })
+
+ it('returns empty map when no lines found', async () => {
+ const period = {
+ period_start: '2025-01-01',
+ opening_balance_entry_id: null,
+ }
+
+ mockFetchAllRows.mockResolvedValue([])
+
+ const { balances } = await getOpeningBalances(supabase, 'company-1', period)
+
+ expect(balances.size).toBe(0)
+ })
+})
diff --git a/lib/reports/__tests__/vat-declaration.test.ts b/lib/reports/__tests__/vat-declaration.test.ts
index b552b180..41573d96 100644
--- a/lib/reports/__tests__/vat-declaration.test.ts
+++ b/lib/reports/__tests__/vat-declaration.test.ts
@@ -817,4 +817,50 @@ describe('calculateVatDeclaration — reverse charge', () => {
// Only the posted entry's invoice (5000) should count
expect(result.rutor.ruta21).toBe(5000)
})
+
+ it('handles zero output VAT on some rates but non-zero on others', async () => {
+ // Only 12% sales in period — no 25% or 6% activity
+ results = [
+ {
+ data: [
+ { account_number: '2621', debit_amount: 0, credit_amount: 600 },
+ { account_number: '3002', debit_amount: 0, credit_amount: 5000 },
+ { account_number: '2641', debit_amount: 200, credit_amount: 0 },
+ ],
+ error: null,
+ },
+ { data: [], error: null },
+ { data: [{ source_type: 'invoice_created' }], error: null },
+ ]
+
+ const result = await calculateVatDeclaration(supabase, 'company-1', 'monthly', 2024, 1)
+
+ expect(result.rutor.ruta10).toBe(0) // no 25% output VAT
+ expect(result.rutor.ruta11).toBe(600) // 12% output VAT
+ expect(result.rutor.ruta12).toBe(0) // no 6% output VAT
+ expect(result.rutor.ruta48).toBe(200)
+ // ruta49 = (0 + 600 + 0 + 0 + 0 + 0 + 0 + 0 + 0) - 200 = 400
+ expect(result.rutor.ruta49).toBe(400)
+ })
+
+ it('includes sub-öre ledger amounts in ruta sums (no threshold filtering)', async () => {
+ results = [
+ {
+ data: [
+ // Very small amount — VAT declaration uses raw summation, no 0.005 filtering
+ { account_number: '2611', debit_amount: 0, credit_amount: 0.001 },
+ { account_number: '3001', debit_amount: 0, credit_amount: 0.004 },
+ ],
+ error: null,
+ },
+ { data: [], error: null },
+ { data: [], error: null },
+ ]
+
+ const result = await calculateVatDeclaration(supabase, 'company-1', 'monthly', 2024, 1)
+
+ // Sub-öre amounts still included in ruta sums
+ expect(result.rutor.ruta10).toBe(0) // rounded: Math.round(0.001 * 100) / 100 = 0
+ expect(result.rutor.ruta05).toBe(0) // rounded: Math.round(0.004 * 100) / 100 = 0
+ })
})
diff --git a/next.config.ts b/next.config.ts
index c8857519..723d11bb 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,5 +1,4 @@
import type { NextConfig } from "next";
-import { withSentryConfig } from "@sentry/nextjs";
const isDev = process.env.NODE_ENV === "development";
@@ -9,7 +8,7 @@ const activepiecesUrl = process.env.ACTIVEPIECES_URL ?? "";
const cspDirectives = [
"default-src 'self'",
- `connect-src 'self' ${supabaseUrl} https://*.supabase.co wss://*.supabase.co https://*.ingest.sentry.io https://*.enablebanking.com https://*.recapt.app`,
+ `connect-src 'self' ${supabaseUrl} https://*.supabase.co wss://*.supabase.co https://*.enablebanking.com https://*.recapt.app`,
`style-src 'self' 'unsafe-inline' https://*.enablebanking.com`,
`script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ""} https://*.enablebanking.com https://cdn.recapt.app`,
"img-src 'self' data: blob: https:",
@@ -20,7 +19,7 @@ const cspDirectives = [
].join("; ");
const nextConfig: NextConfig = {
- output: process.env.VERCEL ? 'standalone' : undefined,
+ output: 'standalone',
async redirects() {
return [
{
@@ -65,9 +64,4 @@ const nextConfig: NextConfig = {
},
};
-export default withSentryConfig(nextConfig, {
- silent: !process.env.SENTRY_AUTH_TOKEN,
- org: process.env.SENTRY_ORG,
- project: process.env.SENTRY_PROJECT,
- ...(process.env.SENTRY_AUTH_TOKEN ? {} : { sourcemaps: { disable: true } }),
-});
+export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 3ed9e246..b83c9d9d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,7 +23,6 @@
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-pdf/renderer": "^4.3.2",
- "@sentry/nextjs": "^10.40.0",
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.93.1",
"@tailwindcss/typography": "^0.5.19",
@@ -802,6 +801,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz",
"integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
@@ -816,6 +816,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz",
"integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -825,6 +826,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz",
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
@@ -855,6 +857,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz",
"integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.6",
@@ -871,6 +874,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.28.6",
@@ -887,6 +891,7 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -896,6 +901,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.28.6",
@@ -909,6 +915,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.28.6",
@@ -926,6 +933,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -935,6 +943,7 @@
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -944,6 +953,7 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -953,6 +963,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
"integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.28.6",
@@ -966,6 +977,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz",
"integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.6"
@@ -990,6 +1002,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
@@ -1004,6 +1017,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz",
"integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
@@ -1022,6 +1036,7 @@
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz",
"integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -1650,96 +1665,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
- "node_modules/@fastify/otel": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@fastify/otel/-/otel-0.16.0.tgz",
- "integrity": "sha512-2304BdM5Q/kUvQC9qJO1KZq3Zn1WWsw+WWkVmFEaj1UE2hEIiuFqrPeglQOwEtw/ftngisqfQ3v70TWMmwhhHA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fastify"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/fastify"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "@opentelemetry/core": "^2.0.0",
- "@opentelemetry/instrumentation": "^0.208.0",
- "@opentelemetry/semantic-conventions": "^1.28.0",
- "minimatch": "^10.0.3"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.9.0"
- }
- },
- "node_modules/@fastify/otel/node_modules/@opentelemetry/api-logs": {
- "version": "0.208.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz",
- "integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api": "^1.3.0"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/@fastify/otel/node_modules/@opentelemetry/instrumentation": {
- "version": "0.208.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz",
- "integrity": "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api-logs": "0.208.0",
- "import-in-the-middle": "^2.0.0",
- "require-in-the-middle": "^8.0.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@fastify/otel/node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "license": "MIT",
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/@fastify/otel/node_modules/brace-expansion": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
- "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/@fastify/otel/node_modules/minimatch": {
- "version": "10.2.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
- "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "brace-expansion": "^5.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/@floating-ui/core": {
"version": "1.7.4",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz",
@@ -2311,6 +2236,7 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -2321,6 +2247,7 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -2331,32 +2258,24 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/source-map": {
- "version": "0.3.11",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
- "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.25"
- }
- },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -2818,560 +2737,6 @@
"node": ">=12.4.0"
}
},
- "node_modules/@opentelemetry/api": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
- "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/@opentelemetry/api-logs": {
- "version": "0.211.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.211.0.tgz",
- "integrity": "sha512-swFdZq8MCdmdR22jTVGQDhwqDzcI4M10nhjXkLr1EsIzXgZBqm4ZlmmcWsg3TSNf+3mzgOiqveXmBLZuDi2Lgg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api": "^1.3.0"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/@opentelemetry/context-async-hooks": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.5.1.tgz",
- "integrity": "sha512-MHbu8XxCHcBn6RwvCt2Vpn1WnLMNECfNKYB14LI5XypcgH4IE0/DiVifVR9tAkwPMyLXN8dOoPJfya3IryLQVw==",
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/core": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz",
- "integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation": {
- "version": "0.211.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.211.0.tgz",
- "integrity": "sha512-h0nrZEC/zvI994nhg7EgQ8URIHt0uDTwN90r3qQUdZORS455bbx+YebnGeEuFghUT0HlJSrLF4iHw67f+odY+Q==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api-logs": "0.211.0",
- "import-in-the-middle": "^2.0.0",
- "require-in-the-middle": "^8.0.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-amqplib": {
- "version": "0.58.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.58.0.tgz",
- "integrity": "sha512-fjpQtH18J6GxzUZ+cwNhWUpb71u+DzT7rFkg5pLssDGaEber91Y2WNGdpVpwGivfEluMlNMZumzjEqfg8DeKXQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "^2.0.0",
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.33.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-connect": {
- "version": "0.54.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.54.0.tgz",
- "integrity": "sha512-43RmbhUhqt3uuPnc16cX6NsxEASEtn8z/cYV8Zpt6EP4p2h9s4FNuJ4Q9BbEQ2C0YlCCB/2crO1ruVz/hWt8fA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "^2.0.0",
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.27.0",
- "@types/connect": "3.4.38"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-dataloader": {
- "version": "0.28.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.28.0.tgz",
- "integrity": "sha512-ExXGBp0sUj8yhm6Znhf9jmuOaGDsYfDES3gswZnKr4MCqoBWQdEFn6EoDdt5u+RdbxQER+t43FoUihEfTSqsjA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.211.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-express": {
- "version": "0.59.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.59.0.tgz",
- "integrity": "sha512-pMKV/qnHiW/Q6pmbKkxt0eIhuNEtvJ7sUAyee192HErlr+a1Jx+FZ3WjfmzhQL1geewyGEiPGkmjjAgNY8TgDA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "^2.0.0",
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.27.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-fs": {
- "version": "0.30.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.30.0.tgz",
- "integrity": "sha512-n3Cf8YhG7reaj5dncGlRIU7iT40bxPOjsBEA5Bc1a1g6e9Qvb+JFJ7SEiMlPbUw4PBmxE3h40ltE8LZ3zVt6OA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "^2.0.0",
- "@opentelemetry/instrumentation": "^0.211.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-generic-pool": {
- "version": "0.54.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.54.0.tgz",
- "integrity": "sha512-8dXMBzzmEdXfH/wjuRvcJnUFeWzZHUnExkmFJ2uPfa31wmpyBCMxO59yr8f/OXXgSogNgi/uPo9KW9H7LMIZ+g==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.211.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-graphql": {
- "version": "0.58.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.58.0.tgz",
- "integrity": "sha512-+yWVVY7fxOs3j2RixCbvue8vUuJ1inHxN2q1sduqDB0Wnkr4vOzVKRYl/Zy7B31/dcPS72D9lo/kltdOTBM3bQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.211.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-hapi": {
- "version": "0.57.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.57.0.tgz",
- "integrity": "sha512-Os4THbvls8cTQTVA8ApLfZZztuuqGEeqog0XUnyRW7QVF0d/vOVBEcBCk1pazPFmllXGEdNbbat8e2fYIWdFbw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "^2.0.0",
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.27.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-http": {
- "version": "0.211.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.211.0.tgz",
- "integrity": "sha512-n0IaQ6oVll9PP84SjbOCwDjaJasWRHi6BLsbMLiT6tNj7QbVOkuA5sk/EfZczwI0j5uTKl1awQPivO/ldVtsqA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "2.5.0",
- "@opentelemetry/instrumentation": "0.211.0",
- "@opentelemetry/semantic-conventions": "^1.29.0",
- "forwarded-parse": "2.1.2"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-http/node_modules/@opentelemetry/core": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
- "integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.0.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-ioredis": {
- "version": "0.59.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.59.0.tgz",
- "integrity": "sha512-875UxzBHWkW+P4Y45SoFM2AR8f8TzBMD8eO7QXGCyFSCUMP5s9vtt/BS8b/r2kqLyaRPK6mLbdnZznK3XzQWvw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/redis-common": "^0.38.2",
- "@opentelemetry/semantic-conventions": "^1.33.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-kafkajs": {
- "version": "0.20.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.20.0.tgz",
- "integrity": "sha512-yJXOuWZROzj7WmYCUiyT27tIfqBrVtl1/TwVbQyWPz7rL0r1Lu7kWjD0PiVeTCIL6CrIZ7M2s8eBxsTAOxbNvw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.30.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-knex": {
- "version": "0.55.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.55.0.tgz",
- "integrity": "sha512-FtTL5DUx5Ka/8VK6P1VwnlUXPa3nrb7REvm5ddLUIeXXq4tb9pKd+/ThB1xM/IjefkRSN3z8a5t7epYw1JLBJQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.33.1"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-koa": {
- "version": "0.59.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.59.0.tgz",
- "integrity": "sha512-K9o2skADV20Skdu5tG2bogPKiSpXh4KxfLjz6FuqIVvDJNibwSdu5UvyyBzRVp1rQMV6UmoIk6d3PyPtJbaGSg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "^2.0.0",
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.36.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.9.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-lru-memoizer": {
- "version": "0.55.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.55.0.tgz",
- "integrity": "sha512-FDBfT7yDGcspN0Cxbu/k8A0Pp1Jhv/m7BMTzXGpcb8ENl3tDj/51U65R5lWzUH15GaZA15HQ5A5wtafklxYj7g==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.211.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-mongodb": {
- "version": "0.64.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.64.0.tgz",
- "integrity": "sha512-pFlCJjweTqVp7B220mCvCld1c1eYKZfQt1p3bxSbcReypKLJTwat+wbL2YZoX9jPi5X2O8tTKFEOahO5ehQGsA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.33.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-mongoose": {
- "version": "0.57.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.57.0.tgz",
- "integrity": "sha512-MthiekrU/BAJc5JZoZeJmo0OTX6ycJMiP6sMOSRTkvz5BrPMYDqaJos0OgsLPL/HpcgHP7eo5pduETuLguOqcg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "^2.0.0",
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.33.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-mysql": {
- "version": "0.57.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.57.0.tgz",
- "integrity": "sha512-HFS/+FcZ6Q7piM7Il7CzQ4VHhJvGMJWjx7EgCkP5AnTntSN5rb5Xi3TkYJHBKeR27A0QqPlGaCITi93fUDs++Q==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.33.0",
- "@types/mysql": "2.15.27"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-mysql2": {
- "version": "0.57.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.57.0.tgz",
- "integrity": "sha512-nHSrYAwF7+aV1E1V9yOOP9TchOodb6fjn4gFvdrdQXiRE7cMuffyLLbCZlZd4wsspBzVwOXX8mpURdRserAhNA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.33.0",
- "@opentelemetry/sql-common": "^0.41.2"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-pg": {
- "version": "0.63.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.63.0.tgz",
- "integrity": "sha512-dKm/ODNN3GgIQVlbD6ZPxwRc3kleLf95hrRWXM+l8wYo+vSeXtEpQPT53afEf6VFWDVzJK55VGn8KMLtSve/cg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "^2.0.0",
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.34.0",
- "@opentelemetry/sql-common": "^0.41.2",
- "@types/pg": "8.15.6",
- "@types/pg-pool": "2.0.7"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-redis": {
- "version": "0.59.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.59.0.tgz",
- "integrity": "sha512-JKv1KDDYA2chJ1PC3pLP+Q9ISMQk6h5ey+99mB57/ARk0vQPGZTTEb4h4/JlcEpy7AYT8HIGv7X6l+br03Neeg==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/redis-common": "^0.38.2",
- "@opentelemetry/semantic-conventions": "^1.27.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-tedious": {
- "version": "0.30.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.30.0.tgz",
- "integrity": "sha512-bZy9Q8jFdycKQ2pAsyuHYUHNmCxCOGdG6eg1Mn75RvQDccq832sU5OWOBnc12EFUELI6icJkhR7+EQKMBam2GA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.33.0",
- "@types/tedious": "^4.0.14"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
- "node_modules/@opentelemetry/instrumentation-undici": {
- "version": "0.21.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.21.0.tgz",
- "integrity": "sha512-gok0LPUOTz2FQ1YJMZzaHcOzDFyT64XJ8M9rNkugk923/p6lDGms/cRW1cqgqp6N6qcd6K6YdVHwPEhnx9BWbw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "^2.0.0",
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/semantic-conventions": "^1.24.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.7.0"
- }
- },
- "node_modules/@opentelemetry/redis-common": {
- "version": "0.38.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.38.2.tgz",
- "integrity": "sha512-1BCcU93iwSRZvDAgwUxC/DV4T/406SkMfxGqu5ojc3AvNI+I9GhV7v0J1HljsczuuhcnFLYqD5VmwVXfCGHzxA==",
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- }
- },
- "node_modules/@opentelemetry/resources": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.1.tgz",
- "integrity": "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "2.5.1",
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/sdk-trace-base": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.1.tgz",
- "integrity": "sha512-iZH3Gw8cxQn0gjpOjJMmKLd9GIaNh/E3v3ST67vyzLSxHBs14HsG4dy7jMYyC5WXGdBVEcM7U/XTF5hCQxjDMw==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "2.5.1",
- "@opentelemetry/resources": "2.5.1",
- "@opentelemetry/semantic-conventions": "^1.29.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": ">=1.3.0 <1.10.0"
- }
- },
- "node_modules/@opentelemetry/semantic-conventions": {
- "version": "1.40.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.40.0.tgz",
- "integrity": "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==",
- "license": "Apache-2.0",
- "engines": {
- "node": ">=14"
- }
- },
- "node_modules/@opentelemetry/sql-common": {
- "version": "0.41.2",
- "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.41.2.tgz",
- "integrity": "sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/core": "^2.0.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.1.0"
- }
- },
- "node_modules/@prisma/instrumentation": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/@prisma/instrumentation/-/instrumentation-7.2.0.tgz",
- "integrity": "sha512-Rh9Z4x5kEj1OdARd7U18AtVrnL6rmLSI0qYShaB4W7Wx5BKbgzndWF+QnuzMb7GLfVdlT5aYCXoPQVYuYtVu0g==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/instrumentation": "^0.207.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.8"
- }
- },
- "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/api-logs": {
- "version": "0.207.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.207.0.tgz",
- "integrity": "sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api": "^1.3.0"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/@prisma/instrumentation/node_modules/@opentelemetry/instrumentation": {
- "version": "0.207.0",
- "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.207.0.tgz",
- "integrity": "sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@opentelemetry/api-logs": "0.207.0",
- "import-in-the-middle": "^2.0.0",
- "require-in-the-middle": "^8.0.0"
- },
- "engines": {
- "node": "^18.19.0 || >=20.6.0"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.3.0"
- }
- },
"node_modules/@radix-ui/number": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@@ -4611,107 +3976,6 @@
"url": "https://opencollective.com/immer"
}
},
- "node_modules/@rollup/plugin-commonjs": {
- "version": "28.0.1",
- "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-28.0.1.tgz",
- "integrity": "sha512-+tNWdlWKbpB3WgBN7ijjYkq9X5uhjmcvyjEght4NmH5fAU++zfQzAJ6wumLS+dNcvwEZhKx2Z+skY8m7v0wGSA==",
- "license": "MIT",
- "dependencies": {
- "@rollup/pluginutils": "^5.0.1",
- "commondir": "^1.0.1",
- "estree-walker": "^2.0.2",
- "fdir": "^6.2.0",
- "is-reference": "1.2.1",
- "magic-string": "^0.30.3",
- "picomatch": "^4.0.2"
- },
- "engines": {
- "node": ">=16.0.0 || 14 >= 14.17"
- },
- "peerDependencies": {
- "rollup": "^2.68.0||^3.0.0||^4.0.0"
- },
- "peerDependenciesMeta": {
- "rollup": {
- "optional": true
- }
- }
- },
- "node_modules/@rollup/plugin-commonjs/node_modules/estree-walker": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
- "license": "MIT"
- },
- "node_modules/@rollup/plugin-commonjs/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/@rollup/plugin-commonjs/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/@rollup/pluginutils": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
- "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
- "license": "MIT",
- "dependencies": {
- "@types/estree": "^1.0.0",
- "estree-walker": "^2.0.2",
- "picomatch": "^4.0.2"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "peerDependencies": {
- "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
- },
- "peerDependenciesMeta": {
- "rollup": {
- "optional": true
- }
- }
- },
- "node_modules/@rollup/pluginutils/node_modules/estree-walker": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
- "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
- "license": "MIT"
- },
- "node_modules/@rollup/pluginutils/node_modules/picomatch": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
- "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
@@ -4719,6 +3983,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4732,6 +3997,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4745,6 +4011,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4758,6 +4025,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4771,6 +4039,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4784,6 +4053,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4797,6 +4067,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4810,6 +4081,7 @@
"cpu": [
"arm"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4823,6 +4095,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4836,6 +4109,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4849,6 +4123,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4862,6 +4137,7 @@
"cpu": [
"loong64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4875,6 +4151,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4888,6 +4165,7 @@
"cpu": [
"ppc64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4901,6 +4179,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4914,6 +4193,7 @@
"cpu": [
"riscv64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4927,6 +4207,7 @@
"cpu": [
"s390x"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4940,6 +4221,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4953,6 +4235,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4966,6 +4249,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4979,6 +4263,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -4992,6 +4277,7 @@
"cpu": [
"arm64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5005,6 +4291,7 @@
"cpu": [
"ia32"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5018,6 +4305,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5031,6 +4319,7 @@
"cpu": [
"x64"
],
+ "dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -5057,507 +4346,6 @@
"url": "https://ko-fi.com/killymxi"
}
},
- "node_modules/@sentry-internal/browser-utils": {
- "version": "10.40.0",
- "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.40.0.tgz",
- "integrity": "sha512-3CDeVNBXYOIvBVdT0SOdMZx5LzYDLuhGK/z7A14sYZz4Cd2+f4mSeFDaEOoH/g2SaY2CKR5KGkAADy8IyjZ21w==",
- "license": "MIT",
- "dependencies": {
- "@sentry/core": "10.40.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@sentry-internal/feedback": {
- "version": "10.40.0",
- "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.40.0.tgz",
- "integrity": "sha512-V/ixkcdCNMo04KgsCEeNEu966xUUTD6czKT2LOAO5siZACqFjT/Rp9VR1n7QQrVo3sL7P3QNiTHtX0jaeWbwzg==",
- "license": "MIT",
- "dependencies": {
- "@sentry/core": "10.40.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@sentry-internal/replay": {
- "version": "10.40.0",
- "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.40.0.tgz",
- "integrity": "sha512-vsH2Ut0KIIQIHNdS3zzEGLJ2C9btbpvJIWAVk7l7oft66JzlUNC89qNaQ5SAypjLQx4Ln2V/ZTqfEoNzXOAsoQ==",
- "license": "MIT",
- "dependencies": {
- "@sentry-internal/browser-utils": "10.40.0",
- "@sentry/core": "10.40.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@sentry-internal/replay-canvas": {
- "version": "10.40.0",
- "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.40.0.tgz",
- "integrity": "sha512-wzQwilFHO2baeCt0dTMf0eW+rgK8O+mkisf9sQzPXzG3Krr/iVtFg1T5T1Th3YsCsEdn6yQ3hcBPLEXjMSvccg==",
- "license": "MIT",
- "dependencies": {
- "@sentry-internal/replay": "10.40.0",
- "@sentry/core": "10.40.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@sentry/babel-plugin-component-annotate": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-5.1.1.tgz",
- "integrity": "sha512-x2wEpBHwsTyTF2rWsLKJlzrRF1TTIGOfX+ngdE+Yd5DBkoS58HwQv824QOviPGQRla4/ypISqAXzjdDPL/zalg==",
- "license": "MIT",
- "engines": {
- "node": ">= 18"
- }
- },
- "node_modules/@sentry/browser": {
- "version": "10.40.0",
- "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.40.0.tgz",
- "integrity": "sha512-nCt3FKUMFad0C6xl5wCK0Jz+qT4Vev4fv6HJRn0YoNRRDQCfsUVxAz7pNyyiPNGM/WCDp9wJpGJsRvbBRd2anw==",
- "license": "MIT",
- "dependencies": {
- "@sentry-internal/browser-utils": "10.40.0",
- "@sentry-internal/feedback": "10.40.0",
- "@sentry-internal/replay": "10.40.0",
- "@sentry-internal/replay-canvas": "10.40.0",
- "@sentry/core": "10.40.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@sentry/bundler-plugin-core": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/@sentry/bundler-plugin-core/-/bundler-plugin-core-5.1.1.tgz",
- "integrity": "sha512-F+itpwR9DyQR7gEkrXd2tigREPTvtF5lC8qu6e4anxXYRTui1+dVR0fXNwjpyAZMhIesLfXRN7WY7ggdj7hi0Q==",
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.18.5",
- "@sentry/babel-plugin-component-annotate": "5.1.1",
- "@sentry/cli": "^2.58.5",
- "dotenv": "^16.3.1",
- "find-up": "^5.0.0",
- "glob": "^13.0.6",
- "magic-string": "~0.30.8"
- },
- "engines": {
- "node": ">= 18"
- }
- },
- "node_modules/@sentry/bundler-plugin-core/node_modules/dotenv": {
- "version": "16.6.1",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
- "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://dotenvx.com"
- }
- },
- "node_modules/@sentry/cli": {
- "version": "2.58.5",
- "resolved": "https://registry.npmjs.org/@sentry/cli/-/cli-2.58.5.tgz",
- "integrity": "sha512-tavJ7yGUZV+z3Ct2/ZB6mg339i08sAk6HDkgqmSRuQEu2iLS5sl9HIvuXfM6xjv8fwlgFOSy++WNABNAcGHUbg==",
- "hasInstallScript": true,
- "license": "FSL-1.1-MIT",
- "dependencies": {
- "https-proxy-agent": "^5.0.0",
- "node-fetch": "^2.6.7",
- "progress": "^2.0.3",
- "proxy-from-env": "^1.1.0",
- "which": "^2.0.2"
- },
- "bin": {
- "sentry-cli": "bin/sentry-cli"
- },
- "engines": {
- "node": ">= 10"
- },
- "optionalDependencies": {
- "@sentry/cli-darwin": "2.58.5",
- "@sentry/cli-linux-arm": "2.58.5",
- "@sentry/cli-linux-arm64": "2.58.5",
- "@sentry/cli-linux-i686": "2.58.5",
- "@sentry/cli-linux-x64": "2.58.5",
- "@sentry/cli-win32-arm64": "2.58.5",
- "@sentry/cli-win32-i686": "2.58.5",
- "@sentry/cli-win32-x64": "2.58.5"
- }
- },
- "node_modules/@sentry/cli-darwin": {
- "version": "2.58.5",
- "resolved": "https://registry.npmjs.org/@sentry/cli-darwin/-/cli-darwin-2.58.5.tgz",
- "integrity": "sha512-lYrNzenZFJftfwSya7gwrHGxtE+Kob/e1sr9lmHMFOd4utDlmq0XFDllmdZAMf21fxcPRI1GL28ejZ3bId01fQ==",
- "license": "FSL-1.1-MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-linux-arm": {
- "version": "2.58.5",
- "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm/-/cli-linux-arm-2.58.5.tgz",
- "integrity": "sha512-KtHweSIomYL4WVDrBrYSYJricKAAzxUgX86kc6OnlikbyOhoK6Fy8Vs6vwd52P6dvWPjgrMpUYjW2M5pYXQDUw==",
- "cpu": [
- "arm"
- ],
- "license": "FSL-1.1-MIT",
- "optional": true,
- "os": [
- "linux",
- "freebsd",
- "android"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-linux-arm64": {
- "version": "2.58.5",
- "resolved": "https://registry.npmjs.org/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.58.5.tgz",
- "integrity": "sha512-/4gywFeBqRB6tR/iGMRAJ3HRqY6Z7Yp4l8ZCbl0TDLAfHNxu7schEw4tSnm2/Hh9eNMiOVy4z58uzAWlZXAYBQ==",
- "cpu": [
- "arm64"
- ],
- "license": "FSL-1.1-MIT",
- "optional": true,
- "os": [
- "linux",
- "freebsd",
- "android"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-linux-i686": {
- "version": "2.58.5",
- "resolved": "https://registry.npmjs.org/@sentry/cli-linux-i686/-/cli-linux-i686-2.58.5.tgz",
- "integrity": "sha512-G7261dkmyxqlMdyvyP06b+RTIVzp1gZNgglj5UksxSouSUqRd/46W/2pQeOMPhloDYo9yLtCN2YFb3Mw4aUsWw==",
- "cpu": [
- "x86",
- "ia32"
- ],
- "license": "FSL-1.1-MIT",
- "optional": true,
- "os": [
- "linux",
- "freebsd",
- "android"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-linux-x64": {
- "version": "2.58.5",
- "resolved": "https://registry.npmjs.org/@sentry/cli-linux-x64/-/cli-linux-x64-2.58.5.tgz",
- "integrity": "sha512-rP04494RSmt86xChkQ+ecBNRYSPbyXc4u0IA7R7N1pSLCyO74e5w5Al+LnAq35cMfVbZgz5Sm0iGLjyiUu4I1g==",
- "cpu": [
- "x64"
- ],
- "license": "FSL-1.1-MIT",
- "optional": true,
- "os": [
- "linux",
- "freebsd",
- "android"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-win32-arm64": {
- "version": "2.58.5",
- "resolved": "https://registry.npmjs.org/@sentry/cli-win32-arm64/-/cli-win32-arm64-2.58.5.tgz",
- "integrity": "sha512-AOJ2nCXlQL1KBaCzv38m3i2VmSHNurUpm7xVKd6yAHX+ZoVBI8VT0EgvwmtJR2TY2N2hNCC7UrgRmdUsQ152bA==",
- "cpu": [
- "arm64"
- ],
- "license": "FSL-1.1-MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-win32-i686": {
- "version": "2.58.5",
- "resolved": "https://registry.npmjs.org/@sentry/cli-win32-i686/-/cli-win32-i686-2.58.5.tgz",
- "integrity": "sha512-EsuboLSOnlrN7MMPJ1eFvfMDm+BnzOaSWl8eYhNo8W/BIrmNgpRUdBwnWn9Q2UOjJj5ZopukmsiMYtU/D7ml9g==",
- "cpu": [
- "x86",
- "ia32"
- ],
- "license": "FSL-1.1-MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli-win32-x64": {
- "version": "2.58.5",
- "resolved": "https://registry.npmjs.org/@sentry/cli-win32-x64/-/cli-win32-x64-2.58.5.tgz",
- "integrity": "sha512-IZf+XIMiQwj+5NzqbOQfywlOitmCV424Vtf9c+ep61AaVScUFD1TSrQbOcJJv5xGxhlxNOMNgMeZhdexdzrKZg==",
- "cpu": [
- "x64"
- ],
- "license": "FSL-1.1-MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@sentry/cli/node_modules/agent-base": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
- "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "4"
- },
- "engines": {
- "node": ">= 6.0.0"
- }
- },
- "node_modules/@sentry/cli/node_modules/https-proxy-agent": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
- "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
- "license": "MIT",
- "dependencies": {
- "agent-base": "6",
- "debug": "4"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/@sentry/core": {
- "version": "10.40.0",
- "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.40.0.tgz",
- "integrity": "sha512-/wrcHPp9Avmgl6WBimPjS4gj810a1wU5oX9fF1bzJfeIIbF3jTsAbv0oMbgDp0cSDnkwv2+NvcPnn3+c5J6pBA==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@sentry/nextjs": {
- "version": "10.40.0",
- "resolved": "https://registry.npmjs.org/@sentry/nextjs/-/nextjs-10.40.0.tgz",
- "integrity": "sha512-0aID+iQ/8oEfmB2j8RRnQqio0AQcxTMiuEV+ev8K64UqJOb64cXNGBYP7fAankd0/jQOvIOuHvZhoZi9pwiRbg==",
- "license": "MIT",
- "dependencies": {
- "@opentelemetry/api": "^1.9.0",
- "@opentelemetry/semantic-conventions": "^1.37.0",
- "@rollup/plugin-commonjs": "28.0.1",
- "@sentry-internal/browser-utils": "10.40.0",
- "@sentry/bundler-plugin-core": "^5.1.0",
- "@sentry/core": "10.40.0",
- "@sentry/node": "10.40.0",
- "@sentry/opentelemetry": "10.40.0",
- "@sentry/react": "10.40.0",
- "@sentry/vercel-edge": "10.40.0",
- "@sentry/webpack-plugin": "^5.1.0",
- "rollup": "^4.35.0",
- "stacktrace-parser": "^0.1.10"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "next": "^13.2.0 || ^14.0 || ^15.0.0-rc.0 || ^16.0.0-0"
- }
- },
- "node_modules/@sentry/node": {
- "version": "10.40.0",
- "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.40.0.tgz",
- "integrity": "sha512-HQETLoNZTUUM8PBxFPT4X0qepzk5NcyWg3jyKUmF7Hh/19KSJItBXXZXxx+8l3PC2eASXUn70utXi65PoXEHWA==",
- "license": "MIT",
- "dependencies": {
- "@fastify/otel": "0.16.0",
- "@opentelemetry/api": "^1.9.0",
- "@opentelemetry/context-async-hooks": "^2.5.1",
- "@opentelemetry/core": "^2.5.1",
- "@opentelemetry/instrumentation": "^0.211.0",
- "@opentelemetry/instrumentation-amqplib": "0.58.0",
- "@opentelemetry/instrumentation-connect": "0.54.0",
- "@opentelemetry/instrumentation-dataloader": "0.28.0",
- "@opentelemetry/instrumentation-express": "0.59.0",
- "@opentelemetry/instrumentation-fs": "0.30.0",
- "@opentelemetry/instrumentation-generic-pool": "0.54.0",
- "@opentelemetry/instrumentation-graphql": "0.58.0",
- "@opentelemetry/instrumentation-hapi": "0.57.0",
- "@opentelemetry/instrumentation-http": "0.211.0",
- "@opentelemetry/instrumentation-ioredis": "0.59.0",
- "@opentelemetry/instrumentation-kafkajs": "0.20.0",
- "@opentelemetry/instrumentation-knex": "0.55.0",
- "@opentelemetry/instrumentation-koa": "0.59.0",
- "@opentelemetry/instrumentation-lru-memoizer": "0.55.0",
- "@opentelemetry/instrumentation-mongodb": "0.64.0",
- "@opentelemetry/instrumentation-mongoose": "0.57.0",
- "@opentelemetry/instrumentation-mysql": "0.57.0",
- "@opentelemetry/instrumentation-mysql2": "0.57.0",
- "@opentelemetry/instrumentation-pg": "0.63.0",
- "@opentelemetry/instrumentation-redis": "0.59.0",
- "@opentelemetry/instrumentation-tedious": "0.30.0",
- "@opentelemetry/instrumentation-undici": "0.21.0",
- "@opentelemetry/resources": "^2.5.1",
- "@opentelemetry/sdk-trace-base": "^2.5.1",
- "@opentelemetry/semantic-conventions": "^1.39.0",
- "@prisma/instrumentation": "7.2.0",
- "@sentry/core": "10.40.0",
- "@sentry/node-core": "10.40.0",
- "@sentry/opentelemetry": "10.40.0",
- "import-in-the-middle": "^2.0.6"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@sentry/node-core": {
- "version": "10.40.0",
- "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.40.0.tgz",
- "integrity": "sha512-ciZGOF54rJH9Fkg7V3v4gmWVufnJRqQQOrn0KStuo49vfPQAJLGePDx+crQv0iNVoLc6Hmrr6E7ebNHSb4NSAw==",
- "license": "MIT",
- "dependencies": {
- "@sentry/core": "10.40.0",
- "@sentry/opentelemetry": "10.40.0",
- "import-in-the-middle": "^2.0.6"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.9.0",
- "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0",
- "@opentelemetry/core": "^1.30.1 || ^2.1.0",
- "@opentelemetry/instrumentation": ">=0.57.1 <1",
- "@opentelemetry/resources": "^1.30.1 || ^2.1.0",
- "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0",
- "@opentelemetry/semantic-conventions": "^1.39.0"
- },
- "peerDependenciesMeta": {
- "@opentelemetry/api": {
- "optional": true
- },
- "@opentelemetry/context-async-hooks": {
- "optional": true
- },
- "@opentelemetry/core": {
- "optional": true
- },
- "@opentelemetry/instrumentation": {
- "optional": true
- },
- "@opentelemetry/resources": {
- "optional": true
- },
- "@opentelemetry/sdk-trace-base": {
- "optional": true
- },
- "@opentelemetry/semantic-conventions": {
- "optional": true
- }
- }
- },
- "node_modules/@sentry/opentelemetry": {
- "version": "10.40.0",
- "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.40.0.tgz",
- "integrity": "sha512-Zx6T258qlEhQfdghIlazSTbK7uRO0pXWw4/4/VPR8pMOiRPh8dAoJg8AB0L55PYPMpVdXxNf7L9X0EZoDYibJw==",
- "license": "MIT",
- "dependencies": {
- "@sentry/core": "10.40.0"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.9.0",
- "@opentelemetry/context-async-hooks": "^1.30.1 || ^2.1.0",
- "@opentelemetry/core": "^1.30.1 || ^2.1.0",
- "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0",
- "@opentelemetry/semantic-conventions": "^1.39.0"
- }
- },
- "node_modules/@sentry/react": {
- "version": "10.40.0",
- "resolved": "https://registry.npmjs.org/@sentry/react/-/react-10.40.0.tgz",
- "integrity": "sha512-3T5W/e3QJMimXRIOx8xMEZbxeIuFiKlXvHLcMTLGygGBYnxQGeb8Oz/8heov+3zF1JoCIxeVQNFW0woySApfyA==",
- "license": "MIT",
- "dependencies": {
- "@sentry/browser": "10.40.0",
- "@sentry/core": "10.40.0"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "react": "^16.14.0 || 17.x || 18.x || 19.x"
- }
- },
- "node_modules/@sentry/vercel-edge": {
- "version": "10.40.0",
- "resolved": "https://registry.npmjs.org/@sentry/vercel-edge/-/vercel-edge-10.40.0.tgz",
- "integrity": "sha512-DdW8F5NE69Wm1CdKTaElFBtTsEzZZlYWs6tkHPY6GapQ97XY+71zu73cx7jFJgCGG/W4l0Em/BQlzNcw4U0V9A==",
- "license": "MIT",
- "dependencies": {
- "@opentelemetry/api": "^1.9.0",
- "@opentelemetry/resources": "^2.5.1",
- "@sentry/core": "10.40.0"
- },
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@sentry/webpack-plugin": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/@sentry/webpack-plugin/-/webpack-plugin-5.1.1.tgz",
- "integrity": "sha512-XgQg+t2aVrlQDfIiAEizqR/bsy6GtBygwgR+Kw11P/cYczj4W9PZ2IYqQEStBzHqnRTh5DbpyMcUNW2CujdA9A==",
- "license": "MIT",
- "dependencies": {
- "@sentry/bundler-plugin-core": "5.1.1",
- "uuid": "^9.0.0"
- },
- "engines": {
- "node": ">= 18"
- },
- "peerDependencies": {
- "webpack": ">=5.0.0"
- }
- },
- "node_modules/@sentry/webpack-plugin/node_modules/uuid": {
- "version": "9.0.1",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
- "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
- "funding": [
- "https://github.com/sponsors/broofa",
- "https://github.com/sponsors/ctavan"
- ],
- "license": "MIT",
- "bin": {
- "uuid": "dist/bin/uuid"
- }
- },
"node_modules/@smithy/config-resolver": {
"version": "4.4.13",
"resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz",
@@ -6619,15 +5407,6 @@
"assertion-error": "^2.0.1"
}
},
- "node_modules/@types/connect": {
- "version": "3.4.38",
- "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
- "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@types/d3-array": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
@@ -6707,28 +5486,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/eslint": {
- "version": "9.6.1",
- "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
- "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@types/estree": "*",
- "@types/json-schema": "*"
- }
- },
- "node_modules/@types/eslint-scope": {
- "version": "3.7.7",
- "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz",
- "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@types/eslint": "*",
- "@types/estree": "*"
- }
- },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -6757,6 +5514,7 @@
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
"license": "MIT"
},
"node_modules/@types/json5": {
@@ -6781,15 +5539,6 @@
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
- "node_modules/@types/mysql": {
- "version": "2.15.27",
- "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.27.tgz",
- "integrity": "sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@types/node": {
"version": "20.19.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
@@ -6799,26 +5548,6 @@
"undici-types": "~6.21.0"
}
},
- "node_modules/@types/pg": {
- "version": "8.15.6",
- "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.15.6.tgz",
- "integrity": "sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*",
- "pg-protocol": "*",
- "pg-types": "^2.2.0"
- }
- },
- "node_modules/@types/pg-pool": {
- "version": "2.0.7",
- "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.7.tgz",
- "integrity": "sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==",
- "license": "MIT",
- "dependencies": {
- "@types/pg": "*"
- }
- },
"node_modules/@types/phoenix": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
@@ -6863,15 +5592,6 @@
"@types/node": "*"
}
},
- "node_modules/@types/tedious": {
- "version": "4.0.14",
- "resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
- "integrity": "sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==",
- "license": "MIT",
- "dependencies": {
- "@types/node": "*"
- }
- },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -7609,181 +6329,6 @@
"url": "https://opencollective.com/vitest"
}
},
- "node_modules/@webassemblyjs/ast": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
- "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/helper-numbers": "1.13.2",
- "@webassemblyjs/helper-wasm-bytecode": "1.13.2"
- }
- },
- "node_modules/@webassemblyjs/floating-point-hex-parser": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz",
- "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/@webassemblyjs/helper-api-error": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz",
- "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/@webassemblyjs/helper-buffer": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz",
- "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/@webassemblyjs/helper-numbers": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz",
- "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/floating-point-hex-parser": "1.13.2",
- "@webassemblyjs/helper-api-error": "1.13.2",
- "@xtuc/long": "4.2.2"
- }
- },
- "node_modules/@webassemblyjs/helper-wasm-bytecode": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz",
- "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/@webassemblyjs/helper-wasm-section": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz",
- "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/ast": "1.14.1",
- "@webassemblyjs/helper-buffer": "1.14.1",
- "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
- "@webassemblyjs/wasm-gen": "1.14.1"
- }
- },
- "node_modules/@webassemblyjs/ieee754": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz",
- "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@xtuc/ieee754": "^1.2.0"
- }
- },
- "node_modules/@webassemblyjs/leb128": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz",
- "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==",
- "license": "Apache-2.0",
- "peer": true,
- "dependencies": {
- "@xtuc/long": "4.2.2"
- }
- },
- "node_modules/@webassemblyjs/utf8": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz",
- "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/@webassemblyjs/wasm-edit": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz",
- "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/ast": "1.14.1",
- "@webassemblyjs/helper-buffer": "1.14.1",
- "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
- "@webassemblyjs/helper-wasm-section": "1.14.1",
- "@webassemblyjs/wasm-gen": "1.14.1",
- "@webassemblyjs/wasm-opt": "1.14.1",
- "@webassemblyjs/wasm-parser": "1.14.1",
- "@webassemblyjs/wast-printer": "1.14.1"
- }
- },
- "node_modules/@webassemblyjs/wasm-gen": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz",
- "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/ast": "1.14.1",
- "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
- "@webassemblyjs/ieee754": "1.13.2",
- "@webassemblyjs/leb128": "1.13.2",
- "@webassemblyjs/utf8": "1.13.2"
- }
- },
- "node_modules/@webassemblyjs/wasm-opt": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz",
- "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/ast": "1.14.1",
- "@webassemblyjs/helper-buffer": "1.14.1",
- "@webassemblyjs/wasm-gen": "1.14.1",
- "@webassemblyjs/wasm-parser": "1.14.1"
- }
- },
- "node_modules/@webassemblyjs/wasm-parser": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz",
- "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/ast": "1.14.1",
- "@webassemblyjs/helper-api-error": "1.13.2",
- "@webassemblyjs/helper-wasm-bytecode": "1.13.2",
- "@webassemblyjs/ieee754": "1.13.2",
- "@webassemblyjs/leb128": "1.13.2",
- "@webassemblyjs/utf8": "1.13.2"
- }
- },
- "node_modules/@webassemblyjs/wast-printer": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz",
- "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@webassemblyjs/ast": "1.14.1",
- "@xtuc/long": "4.2.2"
- }
- },
- "node_modules/@xtuc/ieee754": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
- "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==",
- "license": "BSD-3-Clause",
- "peer": true
- },
- "node_modules/@xtuc/long": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
- "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==",
- "license": "Apache-2.0",
- "peer": true
- },
"node_modules/@zone-eu/mailsplit": {
"version": "5.4.8",
"resolved": "https://registry.npmjs.org/@zone-eu/mailsplit/-/mailsplit-5.4.8.tgz",
@@ -7805,6 +6350,7 @@
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
@@ -7813,28 +6359,6 @@
"node": ">=0.4.0"
}
},
- "node_modules/acorn-import-attributes": {
- "version": "1.9.5",
- "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
- "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
- "license": "MIT",
- "peerDependencies": {
- "acorn": "^8"
- }
- },
- "node_modules/acorn-import-phases": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
- "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=10.13.0"
- },
- "peerDependencies": {
- "acorn": "^8.14.0"
- }
- },
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
@@ -7880,48 +6404,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/ajv-formats": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
- "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependencies": {
- "ajv": "^8.0.0"
- },
- "peerDependenciesMeta": {
- "ajv": {
- "optional": true
- }
- }
- },
- "node_modules/ajv-formats/node_modules/ajv": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
- "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ajv-formats/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "license": "MIT",
- "peer": true
- },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -8323,6 +6805,7 @@
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
"funding": [
{
"type": "opencollective",
@@ -8358,13 +6841,6 @@
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
- "node_modules/buffer-from": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
- "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
- "license": "MIT",
- "peer": true
- },
"node_modules/call-bind": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
@@ -8544,22 +7020,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/chrome-trace-event": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz",
- "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=6.0"
- }
- },
- "node_modules/cjs-module-lexer": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz",
- "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==",
- "license": "MIT"
- },
"node_modules/class-variance-authority": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
@@ -8654,19 +7114,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/commander": {
- "version": "2.20.3",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
- "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/commondir": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
- "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
- "license": "MIT"
- },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -8678,6 +7125,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
"license": "MIT"
},
"node_modules/cookie": {
@@ -9197,6 +7645,7 @@
"version": "1.5.279",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.279.tgz",
"integrity": "sha512-0bblUU5UNdOt5G7XqGiJtpZMONma6WAfq9vsFmtn9x1+joAObr6x1chfqyxFSDCAFwFhCQDrqeAr6MYdpwJ9Hg==",
+ "dev": true,
"license": "ISC"
},
"node_modules/emoji-regex": {
@@ -9225,6 +7674,7 @@
"version": "5.20.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
"integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -9486,6 +7936,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -9899,6 +8350,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
@@ -9911,6 +8363,7 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@@ -10033,23 +8486,6 @@
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
"license": "Unlicense"
},
- "node_modules/fast-uri": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fastify"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/fastify"
- }
- ],
- "license": "BSD-3-Clause",
- "peer": true
- },
"node_modules/fast-xml-builder": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
@@ -10125,6 +8561,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^6.0.0",
@@ -10191,12 +8628,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/forwarded-parse": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz",
- "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==",
- "license": "MIT"
- },
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
@@ -10237,6 +8668,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -10311,6 +8743,7 @@
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -10404,23 +8837,6 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
- "node_modules/glob": {
- "version": "13.0.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
- "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "minimatch": "^10.2.2",
- "minipass": "^7.1.3",
- "path-scurry": "^2.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -10434,49 +8850,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/glob-to-regexp": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
- "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
- "license": "BSD-2-Clause",
- "peer": true
- },
- "node_modules/glob/node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "license": "MIT",
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/glob/node_modules/brace-expansion": {
- "version": "5.0.4",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
- "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/glob/node_modules/minimatch": {
- "version": "10.2.4",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
- "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "brace-expansion": "^5.0.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
@@ -10524,6 +8897,7 @@
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
"license": "ISC"
},
"node_modules/has-bigints": {
@@ -10543,6 +8917,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -10852,18 +9227,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/import-in-the-middle": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-2.0.6.tgz",
- "integrity": "sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==",
- "license": "Apache-2.0",
- "dependencies": {
- "acorn": "^8.15.0",
- "acorn-import-attributes": "^1.9.5",
- "cjs-module-lexer": "^2.2.0",
- "module-details-from-path": "^1.0.4"
- }
- },
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -11251,15 +9614,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/is-reference": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz",
- "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==",
- "license": "MIT",
- "dependencies": {
- "@types/estree": "*"
- }
- },
"node_modules/is-regex": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
@@ -11422,6 +9776,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
"license": "ISC"
},
"node_modules/iterator.prototype": {
@@ -11451,37 +9806,6 @@
"restructure": "^3.0.0"
}
},
- "node_modules/jest-worker": {
- "version": "27.5.1",
- "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
- "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@types/node": "*",
- "merge-stream": "^2.0.0",
- "supports-color": "^8.0.0"
- },
- "engines": {
- "node": ">= 10.13.0"
- }
- },
- "node_modules/jest-worker/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
- },
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -11515,6 +9839,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -11530,13 +9855,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/json-parse-even-better-errors": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
- "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
- "license": "MIT",
- "peer": true
- },
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -11555,6 +9873,7 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
@@ -11999,24 +10318,11 @@
"uc.micro": "^2.0.0"
}
},
- "node_modules/loader-runner": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz",
- "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=6.11.5"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^5.0.0"
@@ -12061,6 +10367,7 @@
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
@@ -12079,6 +10386,7 @@
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
"integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.5"
@@ -12271,13 +10579,6 @@
"integrity": "sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==",
"license": "MIT"
},
- "node_modules/merge-stream": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
- "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
- "license": "MIT",
- "peer": true
- },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -12744,29 +11045,6 @@
"node": ">=8.6"
}
},
- "node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@@ -12795,21 +11073,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/minipass": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
- "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": ">=16 || 14 >=14.17"
- }
- },
- "node_modules/module-details-from-path": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
- "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
- "license": "MIT"
- },
"node_modules/motion-dom": {
"version": "12.29.2",
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.29.2.tgz",
@@ -12872,13 +11135,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/neo-async": {
- "version": "2.6.2",
- "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
- "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
- "license": "MIT",
- "peer": true
- },
"node_modules/next": {
"version": "16.1.5",
"resolved": "https://registry.npmjs.org/next/-/next-16.1.5.tgz",
@@ -12970,30 +11226,11 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/node-fetch": {
- "version": "2.7.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
- "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
- "license": "MIT",
- "dependencies": {
- "whatwg-url": "^5.0.0"
- },
- "engines": {
- "node": "4.x || >=6.0.0"
- },
- "peerDependencies": {
- "encoding": "^0.1.0"
- },
- "peerDependenciesMeta": {
- "encoding": {
- "optional": true
- }
- }
- },
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
"license": "MIT"
},
"node_modules/nodemailer": {
@@ -13187,6 +11424,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
@@ -13202,6 +11440,7 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^3.0.2"
@@ -13326,31 +11565,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/path-scurry": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
- "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "lru-cache": "^11.0.0",
- "minipass": "^7.1.2"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/path-scurry/node_modules/lru-cache": {
- "version": "11.2.6",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz",
- "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==",
- "license": "BlueOak-1.0.0",
- "engines": {
- "node": "20 || >=22"
- }
- },
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -13379,37 +11593,6 @@
"url": "https://ko-fi.com/killymxi"
}
},
- "node_modules/pg-int8": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
- "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
- "license": "ISC",
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/pg-protocol": {
- "version": "1.12.0",
- "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.12.0.tgz",
- "integrity": "sha512-uOANXNRACNdElMXJ0tPz6RBM0XQ61nONGAwlt8da5zs/iUOOCLBQOHSXnrC6fMsvtjxbOJrZZl5IScGv+7mpbg==",
- "license": "MIT"
- },
- "node_modules/pg-types": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
- "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
- "license": "MIT",
- "dependencies": {
- "pg-int8": "1.0.1",
- "postgres-array": "~2.0.0",
- "postgres-bytea": "~1.0.0",
- "postgres-date": "~1.0.4",
- "postgres-interval": "^1.1.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -13496,45 +11679,6 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
- "node_modules/postgres-array": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
- "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/postgres-bytea": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
- "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/postgres-date": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
- "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/postgres-interval": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
- "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
- "license": "MIT",
- "dependencies": {
- "xtend": "^4.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -13551,15 +11695,6 @@
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
- "node_modules/progress": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
- "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -13587,12 +11722,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "license": "MIT"
- },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -13659,16 +11788,6 @@
],
"license": "MIT"
},
- "node_modules/randombytes": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
- "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "safe-buffer": "^5.1.0"
- }
- },
"node_modules/react": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
@@ -14007,19 +12126,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/require-in-the-middle": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz",
- "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.3.5",
- "module-details-from-path": "^1.0.3"
- },
- "engines": {
- "node": ">=9.3.0 || >=8.10.0 <9.0.0"
- }
- },
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
@@ -14125,6 +12231,7 @@
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -14282,63 +12389,6 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
- "node_modules/schema-utils": {
- "version": "4.3.3",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
- "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@types/json-schema": "^7.0.9",
- "ajv": "^8.9.0",
- "ajv-formats": "^2.1.1",
- "ajv-keywords": "^5.1.0"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
- "node_modules/schema-utils/node_modules/ajv": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
- "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/schema-utils/node_modules/ajv-keywords": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
- "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "fast-deep-equal": "^3.1.3"
- },
- "peerDependencies": {
- "ajv": "^8.8.2"
- }
- },
- "node_modules/schema-utils/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
- "license": "MIT",
- "peer": true
- },
"node_modules/selderee": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
@@ -14355,21 +12405,12 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
- "node_modules/serialize-javascript": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
- "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
- "license": "BSD-3-Clause",
- "peer": true,
- "dependencies": {
- "randombytes": "^2.1.0"
- }
- },
"node_modules/server-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
@@ -14608,16 +12649,6 @@
"is-arrayish": "^0.3.1"
}
},
- "node_modules/source-map": {
- "version": "0.6.1",
- "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "license": "BSD-3-Clause",
- "peer": true,
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -14627,17 +12658,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/source-map-support": {
- "version": "0.5.21",
- "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
- "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "buffer-from": "^1.0.0",
- "source-map": "^0.6.0"
- }
- },
"node_modules/space-separated-tokens": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
@@ -14674,27 +12694,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/stacktrace-parser": {
- "version": "0.1.11",
- "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz",
- "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==",
- "license": "MIT",
- "dependencies": {
- "type-fest": "^0.7.1"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/stacktrace-parser/node_modules/type-fest": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz",
- "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==",
- "license": "(MIT OR CC0-1.0)",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/standardwebhooks": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.0.0.tgz",
@@ -15032,6 +13031,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
"integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -15041,60 +13041,6 @@
"url": "https://opencollective.com/webpack"
}
},
- "node_modules/terser": {
- "version": "5.46.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
- "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
- "license": "BSD-2-Clause",
- "peer": true,
- "dependencies": {
- "@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.15.0",
- "commander": "^2.20.0",
- "source-map-support": "~0.5.20"
- },
- "bin": {
- "terser": "bin/terser"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/terser-webpack-plugin": {
- "version": "5.3.16",
- "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz",
- "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@jridgewell/trace-mapping": "^0.3.25",
- "jest-worker": "^27.4.5",
- "schema-utils": "^4.3.0",
- "serialize-javascript": "^6.0.2",
- "terser": "^5.31.1"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependencies": {
- "webpack": "^5.1.0"
- },
- "peerDependenciesMeta": {
- "@swc/core": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
- "uglify-js": {
- "optional": true
- }
- }
- },
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
@@ -15216,12 +13162,6 @@
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
"license": "MIT"
},
- "node_modules/tr46": {
- "version": "0.0.3",
- "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
- "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
- "license": "MIT"
- },
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -15617,6 +13557,7 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
"funding": [
{
"type": "opencollective",
@@ -15985,20 +13926,6 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
- "node_modules/watchpack": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz",
- "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "glob-to-regexp": "^0.4.1",
- "graceful-fs": "^4.1.2"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
"node_modules/web-push": {
"version": "3.6.7",
"resolved": "https://registry.npmjs.org/web-push/-/web-push-3.6.7.tgz",
@@ -16018,116 +13945,11 @@
"node": ">= 16"
}
},
- "node_modules/webidl-conversions": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
- "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
- "license": "BSD-2-Clause"
- },
- "node_modules/webpack": {
- "version": "5.105.3",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.3.tgz",
- "integrity": "sha512-LLBBA4oLmT7sZdHiYE/PeVuifOxYyE2uL/V+9VQP7YSYdJU7bSf7H8bZRRxW8kEPMkmVjnrXmoR3oejIdX0xbg==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "@types/eslint-scope": "^3.7.7",
- "@types/estree": "^1.0.8",
- "@types/json-schema": "^7.0.15",
- "@webassemblyjs/ast": "^1.14.1",
- "@webassemblyjs/wasm-edit": "^1.14.1",
- "@webassemblyjs/wasm-parser": "^1.14.1",
- "acorn": "^8.16.0",
- "acorn-import-phases": "^1.0.3",
- "browserslist": "^4.28.1",
- "chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.19.0",
- "es-module-lexer": "^2.0.0",
- "eslint-scope": "5.1.1",
- "events": "^3.2.0",
- "glob-to-regexp": "^0.4.1",
- "graceful-fs": "^4.2.11",
- "json-parse-even-better-errors": "^2.3.1",
- "loader-runner": "^4.3.1",
- "mime-types": "^2.1.27",
- "neo-async": "^2.6.2",
- "schema-utils": "^4.3.3",
- "tapable": "^2.3.0",
- "terser-webpack-plugin": "^5.3.16",
- "watchpack": "^2.5.1",
- "webpack-sources": "^3.3.4"
- },
- "bin": {
- "webpack": "bin/webpack.js"
- },
- "engines": {
- "node": ">=10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- },
- "peerDependenciesMeta": {
- "webpack-cli": {
- "optional": true
- }
- }
- },
- "node_modules/webpack-sources": {
- "version": "3.3.4",
- "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz",
- "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==",
- "license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/webpack/node_modules/es-module-lexer": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
- "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/webpack/node_modules/eslint-scope": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
- "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
- "license": "BSD-2-Clause",
- "peer": true,
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^4.1.1"
- },
- "engines": {
- "node": ">=8.0.0"
- }
- },
- "node_modules/webpack/node_modules/estraverse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
- "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
- "license": "BSD-2-Clause",
- "peer": true,
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/whatwg-url": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
- "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
- "license": "MIT",
- "dependencies": {
- "tr46": "~0.0.3",
- "webidl-conversions": "^3.0.0"
- }
- },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -16335,15 +14157,6 @@
"node": ">=0.8"
}
},
- "node_modules/xtend": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
- "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4"
- }
- },
"node_modules/y18n": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
@@ -16354,6 +14167,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
"license": "ISC"
},
"node_modules/yargs": {
@@ -16447,6 +14261,7 @@
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
diff --git a/package.json b/package.json
index d68a44d6..6712f19d 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,6 @@
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-pdf/renderer": "^4.3.2",
- "@sentry/nextjs": "^10.40.0",
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.93.1",
"@tailwindcss/typography": "^0.5.19",
diff --git a/sentry.client.config.ts b/sentry.client.config.ts
deleted file mode 100644
index 9eefd0a3..00000000
--- a/sentry.client.config.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as Sentry from "@sentry/nextjs";
-
-const isHosted = process.env.NEXT_PUBLIC_SELF_HOSTED !== "true";
-
-Sentry.init({
- dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
- enabled: isHosted && !!process.env.NEXT_PUBLIC_SENTRY_DSN,
- environment: process.env.NODE_ENV,
- tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
- replaysSessionSampleRate: 0,
- replaysOnErrorSampleRate: 1.0,
- integrations: [Sentry.replayIntegration()],
-});
diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts
deleted file mode 100644
index c80cac49..00000000
--- a/sentry.edge.config.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import * as Sentry from "@sentry/nextjs";
-
-const isHosted = process.env.NEXT_PUBLIC_SELF_HOSTED !== "true";
-
-Sentry.init({
- dsn: process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN,
- enabled: isHosted && !!(process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN),
- environment: process.env.NODE_ENV,
- tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
-});
diff --git a/sentry.server.config.ts b/sentry.server.config.ts
deleted file mode 100644
index c80cac49..00000000
--- a/sentry.server.config.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import * as Sentry from "@sentry/nextjs";
-
-const isHosted = process.env.NEXT_PUBLIC_SELF_HOSTED !== "true";
-
-Sentry.init({
- dsn: process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN,
- enabled: isHosted && !!(process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN),
- environment: process.env.NODE_ENV,
- tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
-});
diff --git a/supabase/migrations/20260407100000_bankid_identities.sql b/supabase/migrations/20260407100000_bankid_identities.sql
deleted file mode 100644
index c903bb0a..00000000
--- a/supabase/migrations/20260407100000_bankid_identities.sql
+++ /dev/null
@@ -1,40 +0,0 @@
--- BankID identity linking table
--- Maps Supabase auth users to Swedish personnummer for BankID login.
--- Personnummer stored as SHA-256 hash (lookup) + AES-256-GCM encrypted (display).
-
-create table public.bankid_identities (
- id uuid primary key default uuid_generate_v4(),
- user_id uuid references auth.users on delete cascade unique not null,
- personal_number_hash text not null,
- personal_number_enc bytea not null,
- given_name text,
- surname text,
- linked_at timestamptz not null default now(),
- created_at timestamptz not null default now(),
- updated_at timestamptz not null default now()
-);
-
-alter table public.bankid_identities enable row level security;
-
--- Users can view their own BankID identity
-create policy "bankid_identities_select" on public.bankid_identities
- for select using (auth.uid() = user_id);
-
--- Users can link BankID to their own account
-create policy "bankid_identities_insert" on public.bankid_identities
- for insert with check (auth.uid() = user_id);
-
--- Users can unlink BankID from their account
-create policy "bankid_identities_delete" on public.bankid_identities
- for delete using (auth.uid() = user_id);
-
--- Fast lookup of returning BankID users by personnummer hash
-create unique index idx_bankid_identities_pnr_hash
- on public.bankid_identities (personal_number_hash);
-
-create index idx_bankid_identities_user_id
- on public.bankid_identities (user_id);
-
-create trigger bankid_identities_updated_at
- before update on public.bankid_identities
- for each row execute function public.update_updated_at_column();
diff --git a/supabase/migrations/20260407120000_add_missing_customer_columns.sql b/supabase/migrations/20260407120000_add_missing_customer_columns.sql
deleted file mode 100644
index 16e20d3d..00000000
--- a/supabase/migrations/20260407120000_add_missing_customer_columns.sql
+++ /dev/null
@@ -1,8 +0,0 @@
--- Add columns to customers that were applied directly to production but never captured in a migration.
--- This ensures staging/preview branches have the same schema.
-
-ALTER TABLE public.customers
- ADD COLUMN IF NOT EXISTS customer_type text NOT NULL DEFAULT 'individual',
- ADD COLUMN IF NOT EXISTS address_line2 text,
- ADD COLUMN IF NOT EXISTS vat_number_validated boolean DEFAULT false,
- ADD COLUMN IF NOT EXISTS default_payment_terms integer DEFAULT 30;
diff --git a/supabase/migrations/20260407120100_add_missing_company_settings_columns.sql b/supabase/migrations/20260407120100_add_missing_company_settings_columns.sql
deleted file mode 100644
index 6cbabcb2..00000000
--- a/supabase/migrations/20260407120100_add_missing_company_settings_columns.sql
+++ /dev/null
@@ -1,23 +0,0 @@
--- Add columns to company_settings that were applied directly to production but never captured in a migration.
--- This ensures staging/preview branches have the same schema.
-
-ALTER TABLE public.company_settings
- ADD COLUMN IF NOT EXISTS bank_name text,
- ADD COLUMN IF NOT EXISTS clearing_number text,
- ADD COLUMN IF NOT EXISTS account_number text,
- ADD COLUMN IF NOT EXISTS selected_sector text,
- ADD COLUMN IF NOT EXISTS selected_modules jsonb DEFAULT '[]'::jsonb,
- ADD COLUMN IF NOT EXISTS business_profile jsonb DEFAULT '{}'::jsonb,
- ADD COLUMN IF NOT EXISTS employee_count integer,
- ADD COLUMN IF NOT EXISTS annual_revenue_range text,
- ADD COLUMN IF NOT EXISTS has_employees boolean DEFAULT false,
- ADD COLUMN IF NOT EXISTS uses_pos_system boolean DEFAULT false,
- ADD COLUMN IF NOT EXISTS sells_internationally boolean DEFAULT false,
- ADD COLUMN IF NOT EXISTS preliminary_tax_monthly numeric,
- ADD COLUMN IF NOT EXISTS next_quote_number integer DEFAULT 1,
- ADD COLUMN IF NOT EXISTS next_order_number integer DEFAULT 1,
- ADD COLUMN IF NOT EXISTS quote_prefix text,
- ADD COLUMN IF NOT EXISTS order_prefix text,
- ADD COLUMN IF NOT EXISTS default_quote_validity_days integer DEFAULT 30,
- ADD COLUMN IF NOT EXISTS swish_number text,
- ADD COLUMN IF NOT EXISTS invoice_default_notes text;
diff --git a/supabase/migrations/20260407120200_sync_missing_schema.sql b/supabase/migrations/20260407120200_sync_missing_schema.sql
deleted file mode 100644
index 9cd60b14..00000000
--- a/supabase/migrations/20260407120200_sync_missing_schema.sql
+++ /dev/null
@@ -1,539 +0,0 @@
--- Sync schema: add all columns and tables that were applied directly to production
--- but never captured in migration files. Uses IF NOT EXISTS throughout for idempotency.
-
--- =============================================================================
--- Missing columns on existing tables
--- =============================================================================
-
-ALTER TABLE public.calendar_feeds
- ADD COLUMN IF NOT EXISTS token_version integer DEFAULT 1;
-
-ALTER TABLE public.cost_centers
- ADD COLUMN IF NOT EXISTS description text,
- ADD COLUMN IF NOT EXISTS manager_name text,
- ADD COLUMN IF NOT EXISTS parent_id uuid,
- ADD COLUMN IF NOT EXISTS sort_order integer DEFAULT 0;
-
-ALTER TABLE public.invoice_inbox_items
- ADD COLUMN IF NOT EXISTS raw_llm_response jsonb;
-
-ALTER TABLE public.invoice_items
- ADD COLUMN IF NOT EXISTS vat_amount numeric NOT NULL DEFAULT 0,
- ADD COLUMN IF NOT EXISTS vat_rate numeric NOT NULL DEFAULT 25;
-
-ALTER TABLE public.invoices
- ADD COLUMN IF NOT EXISTS bankgiro_number text,
- ADD COLUMN IF NOT EXISTS is_recurring boolean DEFAULT false,
- ADD COLUMN IF NOT EXISTS ocr_number text,
- ADD COLUMN IF NOT EXISTS payment_type text,
- ADD COLUMN IF NOT EXISTS plusgiro_number text,
- ADD COLUMN IF NOT EXISTS recurring_invoice_id uuid;
-
-ALTER TABLE public.journal_entry_lines
- ADD COLUMN IF NOT EXISTS cost_center_id uuid,
- ADD COLUMN IF NOT EXISTS project_id uuid;
-
-ALTER TABLE public.projects
- ADD COLUMN IF NOT EXISTS budget_amount numeric DEFAULT 0,
- ADD COLUMN IF NOT EXISTS customer_id uuid,
- ADD COLUMN IF NOT EXISTS description text,
- ADD COLUMN IF NOT EXISTS project_number text,
- ADD COLUMN IF NOT EXISTS status text DEFAULT 'planning';
-
-ALTER TABLE public.receipts
- ADD COLUMN IF NOT EXISTS email_from text,
- ADD COLUMN IF NOT EXISTS representation_business_connection text,
- ADD COLUMN IF NOT EXISTS source text NOT NULL DEFAULT 'upload';
-
--- =============================================================================
--- Missing tables
--- =============================================================================
-
--- voucher_gap_explanations (BFNAR 2013:2 compliance)
-CREATE TABLE IF NOT EXISTS public.voucher_gap_explanations (
- id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
- company_id uuid NOT NULL REFERENCES public.companies ON DELETE CASCADE,
- user_id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
- fiscal_period_id uuid NOT NULL REFERENCES public.fiscal_periods ON DELETE CASCADE,
- voucher_series text NOT NULL DEFAULT 'A',
- gap_start integer NOT NULL,
- gap_end integer NOT NULL,
- explanation text NOT NULL,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz NOT NULL DEFAULT now()
-);
-ALTER TABLE public.voucher_gap_explanations ENABLE ROW LEVEL SECURITY;
-DO $$ BEGIN
- CREATE POLICY "voucher_gap_explanations_select" ON public.voucher_gap_explanations
- FOR SELECT USING (company_id IN (SELECT user_company_ids()));
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "voucher_gap_explanations_insert" ON public.voucher_gap_explanations
- FOR INSERT WITH CHECK (company_id IN (SELECT user_company_ids()));
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "voucher_gap_explanations_update" ON public.voucher_gap_explanations
- FOR UPDATE USING (company_id IN (SELECT user_company_ids()));
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "voucher_gap_explanations_delete" ON public.voucher_gap_explanations
- FOR DELETE USING (company_id IN (SELECT user_company_ids()));
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DROP TRIGGER IF EXISTS voucher_gap_explanations_updated_at ON public.voucher_gap_explanations;
-CREATE TRIGGER voucher_gap_explanations_updated_at
- BEFORE UPDATE ON public.voucher_gap_explanations
- FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-
--- automation_webhooks
-CREATE TABLE IF NOT EXISTS public.automation_webhooks (
- id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
- company_id uuid NOT NULL REFERENCES public.companies ON DELETE CASCADE,
- event_type text NOT NULL,
- webhook_url text NOT NULL,
- active boolean NOT NULL DEFAULT true,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz NOT NULL DEFAULT now()
-);
-ALTER TABLE public.automation_webhooks ENABLE ROW LEVEL SECURITY;
-DO $$ BEGIN
- CREATE POLICY "automation_webhooks_select" ON public.automation_webhooks
- FOR SELECT USING (company_id IN (SELECT user_company_ids()));
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "automation_webhooks_insert" ON public.automation_webhooks
- FOR INSERT WITH CHECK (company_id IN (SELECT user_company_ids()));
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "automation_webhooks_update" ON public.automation_webhooks
- FOR UPDATE USING (company_id IN (SELECT user_company_ids()));
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "automation_webhooks_delete" ON public.automation_webhooks
- FOR DELETE USING (company_id IN (SELECT user_company_ids()));
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DROP TRIGGER IF EXISTS automation_webhooks_updated_at ON public.automation_webhooks;
-CREATE TRIGGER automation_webhooks_updated_at
- BEFORE UPDATE ON public.automation_webhooks
- FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-
--- bankid_identities
-CREATE TABLE IF NOT EXISTS public.bankid_identities (
- id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
- user_id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
- personal_number_hash text NOT NULL,
- personal_number_enc bytea NOT NULL,
- given_name text,
- surname text,
- linked_at timestamptz NOT NULL DEFAULT now(),
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz NOT NULL DEFAULT now()
-);
-ALTER TABLE public.bankid_identities ENABLE ROW LEVEL SECURITY;
-DO $$ BEGIN
- CREATE POLICY "bankid_identities_select" ON public.bankid_identities
- FOR SELECT USING (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "bankid_identities_insert" ON public.bankid_identities
- FOR INSERT WITH CHECK (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "bankid_identities_update" ON public.bankid_identities
- FOR UPDATE USING (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DROP TRIGGER IF EXISTS bankid_identities_updated_at ON public.bankid_identities;
-CREATE TRIGGER bankid_identities_updated_at
- BEFORE UPDATE ON public.bankid_identities
- FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-
--- provider_connections
-CREATE TABLE IF NOT EXISTS public.provider_connections (
- id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
- user_id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
- provider text NOT NULL,
- status text NOT NULL DEFAULT 'pending',
- provider_company_name text,
- error_message text,
- connected_at timestamptz,
- last_synced_at timestamptz,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz NOT NULL DEFAULT now()
-);
-ALTER TABLE public.provider_connections ENABLE ROW LEVEL SECURITY;
-DO $$ BEGIN
- CREATE POLICY "provider_connections_select" ON public.provider_connections
- FOR SELECT USING (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "provider_connections_insert" ON public.provider_connections
- FOR INSERT WITH CHECK (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "provider_connections_update" ON public.provider_connections
- FOR UPDATE USING (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "provider_connections_delete" ON public.provider_connections
- FOR DELETE USING (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DROP TRIGGER IF EXISTS provider_connections_updated_at ON public.provider_connections;
-CREATE TRIGGER provider_connections_updated_at
- BEFORE UPDATE ON public.provider_connections
- FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-
--- provider_connection_tokens
-CREATE TABLE IF NOT EXISTS public.provider_connection_tokens (
- id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
- connection_id uuid NOT NULL REFERENCES public.provider_connections ON DELETE CASCADE,
- access_token text NOT NULL,
- refresh_token text,
- token_expires_at timestamptz,
- provider_company_id text,
- extra_data jsonb DEFAULT '{}'::jsonb,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz NOT NULL DEFAULT now()
-);
-ALTER TABLE public.provider_connection_tokens ENABLE ROW LEVEL SECURITY;
-DROP TRIGGER IF EXISTS provider_connection_tokens_updated_at ON public.provider_connection_tokens;
-CREATE TRIGGER provider_connection_tokens_updated_at
- BEFORE UPDATE ON public.provider_connection_tokens
- FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-
--- provider_oauth_states
-CREATE TABLE IF NOT EXISTS public.provider_oauth_states (
- id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
- user_id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
- provider text NOT NULL,
- csrf_token text NOT NULL,
- connection_id uuid NOT NULL REFERENCES public.provider_connections ON DELETE CASCADE,
- expires_at timestamptz NOT NULL DEFAULT (now() + interval '10 minutes'),
- created_at timestamptz NOT NULL DEFAULT now()
-);
-ALTER TABLE public.provider_oauth_states ENABLE ROW LEVEL SECURITY;
-DO $$ BEGIN
- CREATE POLICY "provider_oauth_states_select" ON public.provider_oauth_states
- FOR SELECT USING (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "provider_oauth_states_insert" ON public.provider_oauth_states
- FOR INSERT WITH CHECK (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "provider_oauth_states_delete" ON public.provider_oauth_states
- FOR DELETE USING (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-
--- provider_sync_data
-CREATE TABLE IF NOT EXISTS public.provider_sync_data (
- id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
- user_id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
- connection_id uuid NOT NULL REFERENCES public.provider_connections ON DELETE CASCADE,
- provider text NOT NULL,
- resource_type text NOT NULL,
- data jsonb NOT NULL DEFAULT '[]'::jsonb,
- record_count integer NOT NULL DEFAULT 0,
- synced_at timestamptz NOT NULL DEFAULT now(),
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz NOT NULL DEFAULT now()
-);
-ALTER TABLE public.provider_sync_data ENABLE ROW LEVEL SECURITY;
-DO $$ BEGIN
- CREATE POLICY "provider_sync_data_select" ON public.provider_sync_data
- FOR SELECT USING (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "provider_sync_data_insert" ON public.provider_sync_data
- FOR INSERT WITH CHECK (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "provider_sync_data_update" ON public.provider_sync_data
- FOR UPDATE USING (auth.uid() = user_id);
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DROP TRIGGER IF EXISTS provider_sync_data_updated_at ON public.provider_sync_data;
-CREATE TRIGGER provider_sync_data_updated_at
- BEFORE UPDATE ON public.provider_sync_data
- FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-
--- email_connections
-CREATE TABLE IF NOT EXISTS public.email_connections (
- id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
- company_id uuid NOT NULL REFERENCES public.companies ON DELETE CASCADE,
- user_id uuid NOT NULL REFERENCES auth.users ON DELETE CASCADE,
- provider text NOT NULL DEFAULT 'gmail',
- email_address text NOT NULL,
- encrypted_token text NOT NULL,
- last_sync_at timestamptz,
- gmail_label_id text,
- status text NOT NULL DEFAULT 'active',
- error_message text,
- created_at timestamptz NOT NULL DEFAULT now(),
- updated_at timestamptz NOT NULL DEFAULT now()
-);
-ALTER TABLE public.email_connections ENABLE ROW LEVEL SECURITY;
-DO $$ BEGIN
- CREATE POLICY "email_connections_select" ON public.email_connections
- FOR SELECT USING (company_id IN (SELECT user_company_ids()));
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "email_connections_insert" ON public.email_connections
- FOR INSERT WITH CHECK (company_id IN (SELECT user_company_ids()));
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "email_connections_update" ON public.email_connections
- FOR UPDATE USING (company_id IN (SELECT user_company_ids()));
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DO $$ BEGIN
- CREATE POLICY "email_connections_delete" ON public.email_connections
- FOR DELETE USING (company_id IN (SELECT user_company_ids()));
-EXCEPTION WHEN duplicate_object THEN NULL;
-END $$;
-DROP TRIGGER IF EXISTS email_connections_updated_at ON public.email_connections;
-CREATE TRIGGER email_connections_updated_at
- BEFORE UPDATE ON public.email_connections
- FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
-
--- =============================================================================
--- Missing functions
--- =============================================================================
-
--- commit_journal_entry — atomic voucher assignment (critical for bookkeeping)
-CREATE OR REPLACE FUNCTION public.commit_journal_entry(p_company_id uuid, p_entry_id uuid)
- RETURNS TABLE(voucher_number integer)
- LANGUAGE plpgsql
- SECURITY DEFINER
-AS $function$
-DECLARE
- v_next integer;
- v_fiscal_period_id uuid;
- v_series text;
-BEGIN
- SELECT je.fiscal_period_id, COALESCE(je.voucher_series, 'A')
- INTO v_fiscal_period_id, v_series
- FROM public.journal_entries je
- WHERE je.id = p_entry_id
- AND je.company_id = p_company_id
- AND je.status = 'draft'
- FOR UPDATE;
-
- IF NOT FOUND THEN
- RAISE EXCEPTION 'Draft journal entry not found: %', p_entry_id;
- END IF;
-
- INSERT INTO public.voucher_sequences (company_id, user_id, fiscal_period_id, voucher_series, last_number)
- VALUES (p_company_id, auth.uid(), v_fiscal_period_id, v_series, 1)
- ON CONFLICT (company_id, fiscal_period_id, voucher_series)
- DO UPDATE SET
- last_number = public.voucher_sequences.last_number + 1,
- updated_at = now()
- RETURNING last_number INTO v_next;
-
- UPDATE public.journal_entries
- SET voucher_number = v_next,
- status = 'posted'
- WHERE id = p_entry_id
- AND company_id = p_company_id;
-
- RETURN QUERY SELECT v_next;
-END;
-$function$;
-
--- release_voucher_range
-CREATE OR REPLACE FUNCTION public.release_voucher_range(p_company_id uuid, p_fiscal_period_id uuid, p_series text, p_actual_last integer, p_reserved_highest integer)
- RETURNS void
- LANGUAGE plpgsql
- SECURITY DEFINER
- SET search_path TO 'public'
-AS $function$
-BEGIN
- UPDATE public.voucher_sequences
- SET last_number = p_actual_last,
- updated_at = now()
- WHERE company_id = p_company_id
- AND fiscal_period_id = p_fiscal_period_id
- AND voucher_series = p_series
- AND last_number > p_actual_last
- AND last_number <= p_reserved_highest;
-END;
-$function$;
-
--- create_invoice_with_items
-CREATE OR REPLACE FUNCTION public.create_invoice_with_items(p_invoice jsonb, p_items jsonb)
- RETURNS jsonb
- LANGUAGE plpgsql
- SECURITY DEFINER
- SET search_path TO 'public'
-AS $function$
-DECLARE
- v_invoice_id uuid;
- v_invoice_number integer;
- v_result jsonb;
-BEGIN
- SELECT COALESCE(MAX(invoice_number::integer), 0) + 1
- INTO v_invoice_number
- FROM invoices
- WHERE user_id = (p_invoice->>'user_id')::uuid;
-
- INSERT INTO invoices (
- user_id, customer_id, invoice_number, invoice_date, due_date,
- status, currency, exchange_rate, exchange_rate_date,
- subtotal, vat_amount, total,
- subtotal_sek, vat_amount_sek, total_sek,
- vat_treatment, vat_rate, moms_ruta,
- your_reference, our_reference, notes,
- reverse_charge_text
- ) VALUES (
- (p_invoice->>'user_id')::uuid,
- (p_invoice->>'customer_id')::uuid,
- v_invoice_number::text,
- (p_invoice->>'invoice_date')::date,
- (p_invoice->>'due_date')::date,
- COALESCE(p_invoice->>'status', 'draft'),
- COALESCE(p_invoice->>'currency', 'SEK'),
- (p_invoice->>'exchange_rate')::numeric,
- (p_invoice->>'exchange_rate_date')::date,
- (p_invoice->>'subtotal')::numeric,
- (p_invoice->>'vat_amount')::numeric,
- (p_invoice->>'total')::numeric,
- (p_invoice->>'subtotal_sek')::numeric,
- (p_invoice->>'vat_amount_sek')::numeric,
- (p_invoice->>'total_sek')::numeric,
- p_invoice->>'vat_treatment',
- (p_invoice->>'vat_rate')::numeric,
- p_invoice->>'moms_ruta',
- p_invoice->>'your_reference',
- p_invoice->>'our_reference',
- p_invoice->>'notes',
- p_invoice->>'reverse_charge_text'
- ) RETURNING id INTO v_invoice_id;
-
- INSERT INTO invoice_items (invoice_id, sort_order, description, quantity, unit, unit_price, line_total)
- SELECT
- v_invoice_id,
- (item->>'sort_order')::integer,
- item->>'description',
- (item->>'quantity')::numeric,
- item->>'unit',
- (item->>'unit_price')::numeric,
- (item->>'line_total')::numeric
- FROM jsonb_array_elements(p_items) AS item;
-
- SELECT jsonb_build_object(
- 'id', i.id,
- 'invoice_number', i.invoice_number,
- 'invoice_date', i.invoice_date,
- 'due_date', i.due_date,
- 'status', i.status,
- 'currency', i.currency,
- 'exchange_rate', i.exchange_rate,
- 'subtotal', i.subtotal,
- 'vat_amount', i.vat_amount,
- 'total', i.total,
- 'subtotal_sek', i.subtotal_sek,
- 'vat_amount_sek', i.vat_amount_sek,
- 'total_sek', i.total_sek,
- 'vat_treatment', i.vat_treatment,
- 'vat_rate', i.vat_rate,
- 'moms_ruta', i.moms_ruta,
- 'your_reference', i.your_reference,
- 'our_reference', i.our_reference,
- 'notes', i.notes,
- 'reverse_charge_text', i.reverse_charge_text,
- 'customer', jsonb_build_object('id', c.id, 'name', c.name),
- 'items', (
- SELECT jsonb_agg(jsonb_build_object(
- 'id', ii.id,
- 'sort_order', ii.sort_order,
- 'description', ii.description,
- 'quantity', ii.quantity,
- 'unit', ii.unit,
- 'unit_price', ii.unit_price,
- 'line_total', ii.line_total
- ) ORDER BY ii.sort_order)
- FROM invoice_items ii WHERE ii.invoice_id = v_invoice_id
- )
- )
- INTO v_result
- FROM invoices i
- LEFT JOIN customers c ON c.id = i.customer_id
- WHERE i.id = v_invoice_id;
-
- RETURN v_result;
-END;
-$function$;
-
--- seed_asset_categories
-CREATE OR REPLACE FUNCTION public.seed_asset_categories(p_user_id uuid)
- RETURNS void
- LANGUAGE plpgsql
- SECURITY DEFINER
- SET search_path TO 'public'
-AS $function$
-begin
- if exists (select 1 from public.asset_categories where user_id = p_user_id) then
- return;
- end if;
-
- insert into public.asset_categories (user_id, code, name, asset_account, depreciation_account, expense_account, default_useful_life_months, default_depreciation_method, is_system)
- values
- (p_user_id, 'BYGGNADER', 'Byggnader', '1110', '1119', '7820', 600, 'straight_line', true),
- (p_user_id, 'MASKINER', 'Maskiner och tekniska anläggningar', '1210', '1219', '7831', 60, 'straight_line', true),
- (p_user_id, 'INVENTARIER', 'Inventarier', '1220', '1229', '7832', 60, 'straight_line', true),
- (p_user_id, 'FORDON', 'Fordon', '1240', '1249', '7834', 60, 'straight_line', true),
- (p_user_id, 'DATORER', 'Datorer och IT-utrustning','1250', '1259', '7833', 36, 'straight_line', true),
- (p_user_id, 'IMMATERIELLA', 'Immateriella tillgångar', '1010', '1019', '7810', 60, 'straight_line', true);
-end;
-$function$;
-
--- update_reconciliation_session_counts (trigger function)
-CREATE OR REPLACE FUNCTION public.update_reconciliation_session_counts()
- RETURNS trigger
- LANGUAGE plpgsql
- SECURITY DEFINER
- SET search_path TO 'public'
-AS $function$
-begin
- update public.bank_reconciliation_sessions
- set
- matched_count = (
- select count(*) from public.bank_reconciliation_items
- where session_id = coalesce(new.session_id, old.session_id)
- and is_reconciled = true
- ),
- unmatched_count = (
- select count(*) from public.bank_reconciliation_items
- where session_id = coalesce(new.session_id, old.session_id)
- and is_reconciled = false
- ),
- total_transactions = (
- select count(*) from public.bank_reconciliation_items
- where session_id = coalesce(new.session_id, old.session_id)
- )
- where id = coalesce(new.session_id, old.session_id);
-
- return coalesce(new, old);
-end;
-$function$;
diff --git a/supabase/migrations/20260408120000_add_missing_delete_policies.sql b/supabase/migrations/20260408120000_add_missing_delete_policies.sql
deleted file mode 100644
index 804861d9..00000000
--- a/supabase/migrations/20260408120000_add_missing_delete_policies.sql
+++ /dev/null
@@ -1,135 +0,0 @@
--- =============================================================================
--- Add missing DELETE RLS policies
--- =============================================================================
--- The multi-tenant migration (20260330130000) dropped all existing policies
--- but only recreated DELETE policies for company_members and api_keys.
--- This migration adds the missing DELETE policies for all tables that need them.
--- =============================================================================
-
--- Direct company_id tables
-CREATE POLICY "customers_delete" ON public.customers
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "suppliers_delete" ON public.suppliers
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "invoices_delete" ON public.invoices
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "invoice_reminders_delete" ON public.invoice_reminders
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "invoice_payments_delete" ON public.invoice_payments
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "supplier_invoices_delete" ON public.supplier_invoices
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "supplier_invoice_payments_delete" ON public.supplier_invoice_payments
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "transactions_delete" ON public.transactions
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "bank_connections_delete" ON public.bank_connections
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "bank_file_imports_delete" ON public.bank_file_imports
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "receipts_delete" ON public.receipts
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "company_settings_delete" ON public.company_settings
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "chart_of_accounts_delete" ON public.chart_of_accounts
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "fiscal_periods_delete" ON public.fiscal_periods
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "journal_entries_delete" ON public.journal_entries
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "mapping_rules_delete" ON public.mapping_rules
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "categorization_templates_delete" ON public.categorization_templates
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "deadlines_delete" ON public.deadlines
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "cost_centers_delete" ON public.cost_centers
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "projects_delete" ON public.projects
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "calendar_feeds_delete" ON public.calendar_feeds
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "extension_data_delete" ON public.extension_data
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "skatteverket_tokens_delete" ON public.skatteverket_tokens
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "document_attachments_delete" ON public.document_attachments
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "invoice_inbox_items_delete" ON public.invoice_inbox_items
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "sie_imports_delete" ON public.sie_imports
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "sie_account_mappings_delete" ON public.sie_account_mappings
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "chat_sessions_delete" ON public.chat_sessions
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
-CREATE POLICY "chat_messages_delete" ON public.chat_messages
- FOR DELETE USING (company_id IN (SELECT public.user_company_ids()));
-
--- Conditional tables (may not exist in all environments)
-DO $$ BEGIN
- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'salary_payments') THEN
- EXECUTE 'CREATE POLICY "salary_payments_delete" ON public.salary_payments FOR DELETE USING (company_id IN (SELECT public.user_company_ids()))';
- END IF;
-END $$;
-
-DO $$ BEGIN
- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'mileage_entries') THEN
- EXECUTE 'CREATE POLICY "mileage_entries_delete" ON public.mileage_entries FOR DELETE USING (company_id IN (SELECT public.user_company_ids()))';
- END IF;
-END $$;
-
-DO $$ BEGIN
- IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'account_balances') THEN
- EXECUTE 'CREATE POLICY "account_balances_delete" ON public.account_balances FOR DELETE USING (company_id IN (SELECT public.user_company_ids()))';
- END IF;
-END $$;
-
--- Sub-tables using parent join (same pattern as their SELECT/INSERT/UPDATE policies)
-CREATE POLICY "invoice_items_delete" ON public.invoice_items
- FOR DELETE USING (
- invoice_id IN (SELECT id FROM public.invoices WHERE company_id IN (SELECT public.user_company_ids()))
- );
-
-CREATE POLICY "journal_entry_lines_delete" ON public.journal_entry_lines
- FOR DELETE USING (
- journal_entry_id IN (SELECT id FROM public.journal_entries WHERE company_id IN (SELECT public.user_company_ids()))
- );
-
-CREATE POLICY "receipt_line_items_delete" ON public.receipt_line_items
- FOR DELETE USING (
- receipt_id IN (SELECT id FROM public.receipts WHERE company_id IN (SELECT public.user_company_ids()))
- );
-
-CREATE POLICY "supplier_invoice_items_delete" ON public.supplier_invoice_items
- FOR DELETE USING (
- supplier_invoice_id IN (SELECT id FROM public.supplier_invoices WHERE company_id IN (SELECT public.user_company_ids()))
- );
diff --git a/supabase/migrations/20260408130000_sie_files_storage_bucket.sql b/supabase/migrations/20260408130000_sie_files_storage_bucket.sql
deleted file mode 100644
index 646d7b0c..00000000
--- a/supabase/migrations/20260408130000_sie_files_storage_bucket.sql
+++ /dev/null
@@ -1,49 +0,0 @@
--- Migration: Create 'sie-files' storage bucket for SIE file archival
--- The SIE import flow archives imported files to Supabase Storage for
--- BFL 7 kap 1-2§ retention compliance, but the bucket was never created.
--- Path convention: {company_id}/{import_id}.se
-
--- =============================================================================
--- 1. Create the 'sie-files' bucket (private, 10MB limit, text only)
--- =============================================================================
-
-INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
-VALUES (
- 'sie-files',
- 'sie-files',
- false,
- 52428800, -- 50 MB, matches MAX_FILE_SIZE in the parse route
- ARRAY['text/plain']
-)
-ON CONFLICT (id) DO NOTHING;
-
--- =============================================================================
--- 2. INSERT policy: Users can upload to companies they belong to
--- =============================================================================
-
-CREATE POLICY "sie_files_insert"
- ON storage.objects
- FOR INSERT
- TO authenticated
- WITH CHECK (
- bucket_id = 'sie-files'
- AND (storage.foldername(name))[1]::uuid IN (SELECT public.user_company_ids())
- );
-
--- =============================================================================
--- 3. SELECT policy: Users can read files from companies they belong to
--- =============================================================================
-
-CREATE POLICY "sie_files_select"
- ON storage.objects
- FOR SELECT
- TO authenticated
- USING (
- bucket_id = 'sie-files'
- AND (storage.foldername(name))[1]::uuid IN (SELECT public.user_company_ids())
- );
-
--- =============================================================================
--- No UPDATE or DELETE policies — WORM compliance for BFL retention
--- Service role bypasses RLS for admin/cron access
--- =============================================================================
diff --git a/supabase/migrations/20260408140000_silent_teams_for_all_users.sql b/supabase/migrations/20260408140000_silent_teams_for_all_users.sql
deleted file mode 100644
index 8f2f54b2..00000000
--- a/supabase/migrations/20260408140000_silent_teams_for_all_users.sql
+++ /dev/null
@@ -1,151 +0,0 @@
--- Migration: Silent teams for all users
---
--- Every user now gets a silent team at signup. This migration:
--- 1. Creates an ensure_user_team() RPC for idempotent team creation
--- 2. Backfills: creates teams for existing users who don't have one
--- 3. Assigns orphaned companies (team_id IS NULL) to their owner's team
--- 4. Deletes incomplete companies (mid-onboarding, no journal entries)
-
--- =============================================================================
--- 1. NEW RPC: ensure_user_team()
--- =============================================================================
--- Idempotently ensures the calling user has a team.
--- Returns the team_id (existing or newly created).
-
-CREATE OR REPLACE FUNCTION public.ensure_user_team()
-RETURNS uuid
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
-AS $$
-DECLARE
- v_user_id uuid;
- v_team_id uuid;
-BEGIN
- v_user_id := auth.uid();
- IF v_user_id IS NULL THEN
- RAISE EXCEPTION 'Not authenticated';
- END IF;
-
- -- Check if user already has a team
- SELECT team_id INTO v_team_id
- FROM public.team_members
- WHERE user_id = v_user_id
- LIMIT 1;
-
- IF v_team_id IS NOT NULL THEN
- RETURN v_team_id;
- END IF;
-
- -- Create a new team (name doesn't matter — hidden from UI)
- INSERT INTO public.teams (name, created_by)
- VALUES ('Personal', v_user_id)
- RETURNING id INTO v_team_id;
-
- -- Add user as team owner
- INSERT INTO public.team_members (team_id, user_id, role)
- VALUES (v_team_id, v_user_id, 'owner');
-
- RETURN v_team_id;
-END;
-$$;
-
-GRANT EXECUTE ON FUNCTION public.ensure_user_team() TO authenticated;
-
--- =============================================================================
--- 2. BACKFILL: Create teams for existing users without one
--- =============================================================================
--- Find all users who have company_members rows but no team_members rows.
--- Create a team for each and add them as owner.
-
-DO $$
-DECLARE
- rec RECORD;
- v_team_id uuid;
-BEGIN
- FOR rec IN
- SELECT DISTINCT cm.user_id
- FROM public.company_members cm
- WHERE NOT EXISTS (
- SELECT 1 FROM public.team_members tm WHERE tm.user_id = cm.user_id
- )
- LOOP
- -- Create team
- INSERT INTO public.teams (name, created_by)
- VALUES ('Personal', rec.user_id)
- RETURNING id INTO v_team_id;
-
- -- Add as owner
- INSERT INTO public.team_members (team_id, user_id, role)
- VALUES (v_team_id, rec.user_id, 'owner');
-
- -- Assign all companies owned by this user to the new team
- UPDATE public.companies
- SET team_id = v_team_id
- WHERE created_by = rec.user_id
- AND team_id IS NULL;
- END LOOP;
-END;
-$$;
-
--- =============================================================================
--- 3. Assign any remaining orphaned companies to their creator's team
--- =============================================================================
--- Edge case: companies where team_id IS NULL but the creator already has a team
--- (e.g., they were a team member but also had solo companies).
-
-UPDATE public.companies c
-SET team_id = (
- SELECT tm.team_id
- FROM public.team_members tm
- WHERE tm.user_id = c.created_by
- LIMIT 1
-)
-WHERE c.team_id IS NULL
- AND EXISTS (
- SELECT 1 FROM public.team_members tm WHERE tm.user_id = c.created_by
- );
-
--- =============================================================================
--- 4. Delete incomplete companies (mid-onboarding cleanup)
--- =============================================================================
--- Only delete companies where:
--- - onboarding_complete is false or no settings row exists
--- - There are zero journal entries
--- - There are zero transactions
--- This is safe because no real bookkeeping data exists.
-
-DO $$
-DECLARE
- rec RECORD;
- v_je_count int;
- v_tx_count int;
-BEGIN
- FOR rec IN
- SELECT c.id AS company_id
- FROM public.companies c
- LEFT JOIN public.company_settings cs ON cs.company_id = c.id
- WHERE (cs.onboarding_complete IS NULL OR cs.onboarding_complete = false)
- LOOP
- -- Check for journal entries
- SELECT count(*) INTO v_je_count
- FROM public.journal_entries
- WHERE company_id = rec.company_id;
-
- -- Check for transactions
- SELECT count(*) INTO v_tx_count
- FROM public.transactions
- WHERE company_id = rec.company_id;
-
- -- Only delete if truly empty
- IF v_je_count = 0 AND v_tx_count = 0 THEN
- -- Delete dependent rows first (order matters for FK constraints)
- DELETE FROM public.company_settings WHERE company_id = rec.company_id;
- DELETE FROM public.fiscal_periods WHERE company_id = rec.company_id;
- DELETE FROM public.chart_of_accounts WHERE company_id = rec.company_id;
- DELETE FROM public.company_members WHERE company_id = rec.company_id;
- DELETE FROM public.companies WHERE id = rec.company_id;
- END IF;
- END LOOP;
-END;
-$$;
diff --git a/supabase/migrations/20260409120000_add_invoice_delivery_date.sql b/supabase/migrations/20260409120000_add_invoice_delivery_date.sql
deleted file mode 100644
index 250e6636..00000000
--- a/supabase/migrations/20260409120000_add_invoice_delivery_date.sql
+++ /dev/null
@@ -1,2 +0,0 @@
--- ML 17:24 p.7: leveransdatum when different from fakturadatum
-ALTER TABLE public.invoices ADD COLUMN IF NOT EXISTS delivery_date date;
diff --git a/supabase/migrations/20260409130000_add_integrity_audit_actions.sql b/supabase/migrations/20260409130000_add_integrity_audit_actions.sql
deleted file mode 100644
index b4199454..00000000
--- a/supabase/migrations/20260409130000_add_integrity_audit_actions.sql
+++ /dev/null
@@ -1,15 +0,0 @@
--- Add INTEGRITY_FAILURE to audit_log action CHECK constraint
--- The verify cron (app/api/documents/verify/cron) inserts INTEGRITY_FAILURE
--- but the original CHECK constraint in migration 014 did not include it,
--- causing all integrity failure logging to silently fail.
-
-ALTER TABLE public.audit_log DROP CONSTRAINT audit_log_action_check;
-
-ALTER TABLE public.audit_log ADD CONSTRAINT audit_log_action_check CHECK (action IN (
- 'INSERT', 'UPDATE', 'DELETE',
- 'COMMIT', 'REVERSE', 'CORRECT',
- 'LOCK_PERIOD', 'CLOSE_PERIOD',
- 'DOCUMENT_DELETE_BLOCKED', 'RETENTION_BLOCK',
- 'SECURITY_EVENT',
- 'INTEGRITY_FAILURE'
-));
diff --git a/supabase/migrations/20260409130001_add_trade_name.sql b/supabase/migrations/20260409130001_add_trade_name.sql
deleted file mode 100644
index e45d2c4b..00000000
--- a/supabase/migrations/20260409130001_add_trade_name.sql
+++ /dev/null
@@ -1,5 +0,0 @@
--- Add trade_name column to company_settings
--- Allows companies to display a trade name (handelsnamn) on invoices
--- and other external-facing documents instead of the legal company name.
-ALTER TABLE public.company_settings
- ADD COLUMN trade_name text;
diff --git a/supabase/migrations/20260409130100_document_version_chain.sql b/supabase/migrations/20260409130100_document_version_chain.sql
deleted file mode 100644
index 831aacea..00000000
--- a/supabase/migrations/20260409130100_document_version_chain.sql
+++ /dev/null
@@ -1,116 +0,0 @@
--- Document version chain: prev_version_hash column + create_document_version RPC
--- Fixes the non-functional document versioning (migration 023 was a placeholder).
--- The createNewVersion() in lib/core/documents/document-service.ts calls this RPC.
-
--- 1. Add prev_version_hash column for cryptographic version chain
-ALTER TABLE public.document_attachments
- ADD COLUMN IF NOT EXISTS prev_version_hash text;
-
--- 2. Atomic version creation RPC
--- Row-locks the current version, inserts a new version with hash chain,
--- and marks the old version as superseded — all in one transaction.
-CREATE OR REPLACE FUNCTION public.create_document_version(
- p_user_id uuid,
- p_original_doc_id uuid,
- p_storage_path text,
- p_file_name text,
- p_file_size_bytes bigint,
- p_mime_type text,
- p_sha256_hash text
-)
-RETURNS uuid
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
-AS $$
-DECLARE
- v_current document_attachments%ROWTYPE;
- v_new_id uuid;
- v_root_id uuid;
- v_next_version integer;
-BEGIN
- -- Lock the current version row to prevent concurrent versioning
- SELECT * INTO v_current
- FROM public.document_attachments
- WHERE id = p_original_doc_id
- AND is_current_version = true
- FOR UPDATE;
-
- IF v_current IS NULL THEN
- RAISE EXCEPTION 'Document % not found or is not the current version', p_original_doc_id;
- END IF;
-
- -- Determine root document and next version number
- v_root_id := COALESCE(v_current.original_id, v_current.id);
- v_next_version := v_current.version + 1;
-
- -- Insert new version with hash chain link
- INSERT INTO public.document_attachments (
- user_id, company_id, storage_path, file_name, file_size_bytes,
- mime_type, sha256_hash, version, original_id, is_current_version,
- uploaded_by, upload_source, digitization_date,
- journal_entry_id, journal_entry_line_id, prev_version_hash
- ) VALUES (
- p_user_id, v_current.company_id, p_storage_path, p_file_name,
- p_file_size_bytes, p_mime_type, p_sha256_hash, v_next_version,
- v_root_id, true, p_user_id, v_current.upload_source, now(),
- v_current.journal_entry_id, v_current.journal_entry_line_id,
- v_current.sha256_hash -- cryptographic link to previous version
- )
- RETURNING id INTO v_new_id;
-
- -- Mark old version as superseded
- UPDATE public.document_attachments
- SET is_current_version = false,
- superseded_by_id = v_new_id
- WHERE id = p_original_doc_id;
-
- RETURN v_new_id;
-END;
-$$;
-
--- 3. Version chain validation function
--- Walks the version chain from newest to oldest and verifies each
--- prev_version_hash matches the prior version's sha256_hash.
-CREATE OR REPLACE FUNCTION public.validate_version_chain(p_document_id uuid)
-RETURNS TABLE(version integer, document_id uuid, hash_valid boolean)
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
-AS $$
-DECLARE
- v_root_id uuid;
-BEGIN
- -- Find root document
- SELECT COALESCE(da.original_id, da.id) INTO v_root_id
- FROM public.document_attachments da
- WHERE da.id = p_document_id;
-
- IF v_root_id IS NULL THEN
- RAISE EXCEPTION 'Document % not found', p_document_id;
- END IF;
-
- -- Walk chain and verify hashes
- RETURN QUERY
- WITH chain AS (
- SELECT
- da.id AS doc_id,
- da.version AS ver,
- da.sha256_hash,
- da.prev_version_hash,
- LAG(da.sha256_hash) OVER (ORDER BY da.version) AS expected_prev_hash
- FROM public.document_attachments da
- WHERE da.id = v_root_id OR da.original_id = v_root_id
- ORDER BY da.version
- )
- SELECT
- chain.ver,
- chain.doc_id,
- CASE
- WHEN chain.ver = 1 THEN chain.prev_version_hash IS NULL
- ELSE chain.prev_version_hash IS NOT DISTINCT FROM chain.expected_prev_hash
- END AS hash_valid
- FROM chain
- ORDER BY chain.ver;
-END;
-$$;
diff --git a/supabase/migrations/20260409130200_enforce_document_metadata_immutability.sql b/supabase/migrations/20260409130200_enforce_document_metadata_immutability.sql
deleted file mode 100644
index 6d717b40..00000000
--- a/supabase/migrations/20260409130200_enforce_document_metadata_immutability.sql
+++ /dev/null
@@ -1,57 +0,0 @@
--- Enforce document metadata immutability for documents linked to committed entries
--- BFL 7 kap requires verifikation underlag to be immutable once committed.
--- Existing triggers only block DELETE — this blocks metadata UPDATE.
-
-CREATE OR REPLACE FUNCTION public.enforce_document_metadata_immutability()
-RETURNS trigger
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
-AS $$
-DECLARE
- v_entry_status text;
-BEGIN
- -- Only enforce on documents already linked to a journal entry
- IF OLD.journal_entry_id IS NULL THEN
- RETURN NEW;
- END IF;
-
- SELECT status INTO v_entry_status
- FROM public.journal_entries
- WHERE id = OLD.journal_entry_id;
-
- -- Only enforce for committed (posted/reversed) entries
- IF v_entry_status IS NULL OR v_entry_status NOT IN ('posted', 'reversed') THEN
- RETURN NEW;
- END IF;
-
- -- Block changes to immutable fields
- -- Allowed: last_integrity_check_at (cron), updated_at (auto-trigger),
- -- superseded_by_id (versioning), prev_version_hash (versioning),
- -- journal_entry_id/journal_entry_line_id (linking)
- IF NEW.file_name IS DISTINCT FROM OLD.file_name
- OR NEW.storage_path IS DISTINCT FROM OLD.storage_path
- OR NEW.file_size_bytes IS DISTINCT FROM OLD.file_size_bytes
- OR NEW.mime_type IS DISTINCT FROM OLD.mime_type
- OR NEW.sha256_hash IS DISTINCT FROM OLD.sha256_hash
- OR NEW.upload_source IS DISTINCT FROM OLD.upload_source
- OR NEW.digitization_date IS DISTINCT FROM OLD.digitization_date
- OR NEW.uploaded_by IS DISTINCT FROM OLD.uploaded_by
- OR NEW.version IS DISTINCT FROM OLD.version
- OR NEW.original_id IS DISTINCT FROM OLD.original_id
- THEN
- -- Log the blocked attempt
- INSERT INTO public.audit_log (user_id, company_id, action, table_name, record_id, description)
- VALUES (OLD.user_id, OLD.company_id, 'SECURITY_EVENT', 'document_attachments', OLD.id,
- 'Blocked metadata modification of document linked to ' || v_entry_status || ' entry ' || OLD.journal_entry_id);
-
- RAISE EXCEPTION 'Cannot modify metadata of document linked to a % journal entry (BFL 7 kap)', v_entry_status;
- END IF;
-
- RETURN NEW;
-END;
-$$;
-
-CREATE TRIGGER enforce_document_metadata_immutability
- BEFORE UPDATE ON public.document_attachments
- FOR EACH ROW EXECUTE FUNCTION public.enforce_document_metadata_immutability();
diff --git a/supabase/migrations/20260409130300_audit_all_document_deletions.sql b/supabase/migrations/20260409130300_audit_all_document_deletions.sql
deleted file mode 100644
index fa855411..00000000
--- a/supabase/migrations/20260409130300_audit_all_document_deletions.sql
+++ /dev/null
@@ -1,67 +0,0 @@
--- Audit all document deletions, including unlinked documents
--- Previously, block_document_deletion() only logged linked document deletion attempts.
--- Unlinked documents were deleted without any trace in the audit log.
-
-CREATE OR REPLACE FUNCTION public.block_document_deletion()
-RETURNS trigger
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
-AS $$
-DECLARE
- v_entry_status text;
- v_retention_expires date;
-BEGIN
- -- Always log document deletion attempts (linked and unlinked)
- IF OLD.journal_entry_id IS NULL THEN
- INSERT INTO public.audit_log (user_id, company_id, action, table_name, record_id, description, old_state)
- VALUES (
- OLD.user_id, OLD.company_id, 'DELETE', 'document_attachments', OLD.id,
- 'Deleted unlinked document "' || OLD.file_name || '"',
- to_jsonb(OLD)
- );
- -- Allow deletion of unlinked documents
- RETURN OLD;
- END IF;
-
- -- Check if linked to a committed journal entry
- SELECT je.status INTO v_entry_status
- FROM public.journal_entries je
- WHERE je.id = OLD.journal_entry_id;
-
- IF v_entry_status IN ('posted', 'reversed') THEN
- -- Log the blocked attempt
- INSERT INTO public.audit_log (user_id, company_id, action, table_name, record_id, description)
- VALUES (OLD.user_id, OLD.company_id, 'DOCUMENT_DELETE_BLOCKED', 'document_attachments', OLD.id,
- 'Attempted deletion of document linked to ' || v_entry_status || ' journal entry ' || OLD.journal_entry_id);
-
- RAISE EXCEPTION 'Cannot delete document linked to a % journal entry (Bokforingslagen)',
- v_entry_status;
- END IF;
-
- -- Check retention window
- SELECT fp.retention_expires_at INTO v_retention_expires
- FROM public.journal_entries je
- JOIN public.fiscal_periods fp ON fp.id = je.fiscal_period_id
- WHERE je.id = OLD.journal_entry_id;
-
- IF v_retention_expires IS NOT NULL AND v_retention_expires > CURRENT_DATE THEN
- INSERT INTO public.audit_log (user_id, company_id, action, table_name, record_id, description)
- VALUES (OLD.user_id, OLD.company_id, 'RETENTION_BLOCK', 'document_attachments', OLD.id,
- 'Attempted deletion within retention period (expires ' || v_retention_expires || ')');
-
- RAISE EXCEPTION 'Cannot delete document within 7-year retention period (expires %)',
- v_retention_expires;
- END IF;
-
- -- Log deletion of linked-but-not-committed documents
- INSERT INTO public.audit_log (user_id, company_id, action, table_name, record_id, description, old_state)
- VALUES (
- OLD.user_id, OLD.company_id, 'DELETE', 'document_attachments', OLD.id,
- 'Deleted document "' || OLD.file_name || '" linked to draft entry ' || OLD.journal_entry_id,
- to_jsonb(OLD)
- );
-
- RETURN OLD;
-END;
-$$;
diff --git a/supabase/migrations/20260409130605_fix_immutability_posted_cancelled.sql b/supabase/migrations/20260409130605_fix_immutability_posted_cancelled.sql
deleted file mode 100644
index 8836ee3f..00000000
--- a/supabase/migrations/20260409130605_fix_immutability_posted_cancelled.sql
+++ /dev/null
@@ -1,35 +0,0 @@
--- Fix: add posted → cancelled transition for orphaned entry cleanup.
--- The live trigger is missing this transition due to migration 20260319000001
--- being edited after it was applied. engine.ts reverseEntry and storno-service
--- need posted → cancelled for CAS guard cleanup of orphaned concurrent reversals.
-
-CREATE OR REPLACE FUNCTION public.enforce_journal_entry_immutability()
-RETURNS trigger LANGUAGE plpgsql AS $$
-BEGIN
- IF TG_OP = 'DELETE' THEN
- -- No exemption for drafts: varaktighet applies from insertion.
- -- Application code uses status='cancelled' instead of DELETE.
- RAISE EXCEPTION 'Cannot delete journal entries (id: %, status: %). Use cancelled status instead.',
- OLD.id, OLD.status;
- END IF;
-
- -- Draft can transition to draft (update fields), posted, or cancelled
- IF OLD.status = 'draft' AND NEW.status IN ('draft', 'posted', 'cancelled') THEN
- RETURN NEW;
- END IF;
-
- -- Posted can transition to reversed (storno) or cancelled (orphaned cleanup)
- IF OLD.status = 'posted' AND NEW.status IN ('reversed', 'cancelled') THEN
- IF NEW.status = 'reversed' THEN
- IF NEW.description != OLD.description OR NEW.entry_date != OLD.entry_date
- OR NEW.fiscal_period_id != OLD.fiscal_period_id
- OR NEW.voucher_number != OLD.voucher_number THEN
- RAISE EXCEPTION 'Cannot modify fields of a posted entry during reversal (id: %)', OLD.id;
- END IF;
- END IF;
- RETURN NEW;
- END IF;
-
- RAISE EXCEPTION 'Cannot modify a % journal entry (id: %). Committed entries are immutable per Bokforingslagen.',
- OLD.status, OLD.id;
-END; $$;
diff --git a/supabase/migrations/20260409140000_fix_delete_user_account_timeout.sql b/supabase/migrations/20260409140000_fix_delete_user_account_timeout.sql
deleted file mode 100644
index 0a5d362e..00000000
--- a/supabase/migrations/20260409140000_fix_delete_user_account_timeout.sql
+++ /dev/null
@@ -1,116 +0,0 @@
--- Fix delete_user_account: timeout + cascade blockers.
---
--- Problem 1: ALTER TABLE DDL requires ACCESS EXCLUSIVE locks. With the
--- authenticated role's 8s statement_timeout, any concurrent read causes
--- error 57014. Fixed via function-level SET statement_timeout = '60s'.
---
--- Problem 2: Several NO ACTION FKs block the CASCADE chain from
--- auth.users → companies → children:
--- audit_log.company_id → companies (NO ACTION)
--- fiscal_periods.previous_period_id → fiscal_periods (NO ACTION, self-ref)
--- fiscal_periods.closing_entry_id → journal_entries (NO ACTION)
--- fiscal_periods.opening_balance_entry_id → journal_entries (NO ACTION)
--- Fixed by explicitly clearing these before the CASCADE fires.
-
-CREATE OR REPLACE FUNCTION public.delete_user_account(target_user_id uuid)
-RETURNS void
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
-SET statement_timeout = '60s'
-SET lock_timeout = '10s'
-AS $$
-DECLARE
- v_company_ids uuid[];
-BEGIN
- -- Only allow users to delete their own account
- IF auth.uid() IS DISTINCT FROM target_user_id THEN
- RAISE EXCEPTION 'Can only delete your own account';
- END IF;
-
- -- Collect companies owned by this user (CASCADE will delete these)
- SELECT array_agg(id) INTO v_company_ids
- FROM public.companies
- WHERE created_by = target_user_id;
-
- -- Clear active_company_id to avoid FK conflicts during CASCADE
- DELETE FROM public.user_preferences WHERE user_id = target_user_id;
-
- -- Explicitly delete from extension_data (missing DELETE RLS policy
- -- causes CASCADE from auth.users to fail even with ON DELETE CASCADE)
- DELETE FROM public.extension_data WHERE user_id = target_user_id;
-
- -- Disable BEFORE DELETE triggers that block deletion
- ALTER TABLE audit_log DISABLE TRIGGER audit_log_no_delete;
- ALTER TABLE payment_match_log DISABLE TRIGGER payment_match_log_no_delete;
- ALTER TABLE document_attachments DISABLE TRIGGER block_document_deletion;
- ALTER TABLE journal_entries DISABLE TRIGGER enforce_journal_entry_immutability;
- ALTER TABLE journal_entries DISABLE TRIGGER enforce_retention_journal_entries;
- ALTER TABLE journal_entry_lines DISABLE TRIGGER enforce_journal_entry_line_immutability;
-
- -- Disable AFTER DELETE audit triggers (they INSERT into audit_log during
- -- CASCADE, which would create orphaned rows after the user is gone)
- ALTER TABLE api_keys DISABLE TRIGGER audit_api_keys;
- ALTER TABLE chart_of_accounts DISABLE TRIGGER audit_chart_of_accounts;
- ALTER TABLE company_settings DISABLE TRIGGER audit_company_settings;
- ALTER TABLE document_attachments DISABLE TRIGGER audit_document_attachments;
- ALTER TABLE extension_data DISABLE TRIGGER audit_extension_data;
- ALTER TABLE fiscal_periods DISABLE TRIGGER audit_fiscal_periods;
- ALTER TABLE journal_entries DISABLE TRIGGER audit_journal_entries;
- ALTER TABLE supplier_invoices DISABLE TRIGGER audit_supplier_invoices;
-
- -- Clear NO ACTION FK references that block the CASCADE chain
- IF v_company_ids IS NOT NULL THEN
- -- audit_log.company_id → companies (NO ACTION)
- DELETE FROM public.audit_log
- WHERE company_id = ANY(v_company_ids);
-
- -- fiscal_periods self-ref and cross-refs to journal_entries (NO ACTION)
- UPDATE public.fiscal_periods
- SET previous_period_id = NULL,
- closing_entry_id = NULL,
- opening_balance_entry_id = NULL
- WHERE company_id = ANY(v_company_ids);
- END IF;
-
- -- Delete from auth.users — ON DELETE CASCADE handles all public tables
- DELETE FROM auth.users WHERE id = target_user_id;
-
- -- Re-enable all triggers
- ALTER TABLE audit_log ENABLE TRIGGER audit_log_no_delete;
- ALTER TABLE payment_match_log ENABLE TRIGGER payment_match_log_no_delete;
- ALTER TABLE document_attachments ENABLE TRIGGER block_document_deletion;
- ALTER TABLE journal_entries ENABLE TRIGGER enforce_journal_entry_immutability;
- ALTER TABLE journal_entries ENABLE TRIGGER enforce_retention_journal_entries;
- ALTER TABLE journal_entry_lines ENABLE TRIGGER enforce_journal_entry_line_immutability;
- ALTER TABLE api_keys ENABLE TRIGGER audit_api_keys;
- ALTER TABLE chart_of_accounts ENABLE TRIGGER audit_chart_of_accounts;
- ALTER TABLE company_settings ENABLE TRIGGER audit_company_settings;
- ALTER TABLE document_attachments ENABLE TRIGGER audit_document_attachments;
- ALTER TABLE extension_data ENABLE TRIGGER audit_extension_data;
- ALTER TABLE fiscal_periods ENABLE TRIGGER audit_fiscal_periods;
- ALTER TABLE journal_entries ENABLE TRIGGER audit_journal_entries;
- ALTER TABLE supplier_invoices ENABLE TRIGGER audit_supplier_invoices;
-
-EXCEPTION WHEN OTHERS THEN
- -- PostgreSQL transactional DDL already rolls back the DISABLE TRIGGER
- -- statements if the function aborts, but re-enable explicitly as a
- -- defensive guard against sub-transaction edge cases so enforcement
- -- triggers are never left disabled on the live tables.
- BEGIN ALTER TABLE audit_log ENABLE TRIGGER audit_log_no_delete; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE payment_match_log ENABLE TRIGGER payment_match_log_no_delete; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE document_attachments ENABLE TRIGGER block_document_deletion; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE journal_entries ENABLE TRIGGER enforce_journal_entry_immutability; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE journal_entries ENABLE TRIGGER enforce_retention_journal_entries; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE journal_entry_lines ENABLE TRIGGER enforce_journal_entry_line_immutability; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE api_keys ENABLE TRIGGER audit_api_keys; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE chart_of_accounts ENABLE TRIGGER audit_chart_of_accounts; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE company_settings ENABLE TRIGGER audit_company_settings; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE document_attachments ENABLE TRIGGER audit_document_attachments; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE extension_data ENABLE TRIGGER audit_extension_data; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE fiscal_periods ENABLE TRIGGER audit_fiscal_periods; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE journal_entries ENABLE TRIGGER audit_journal_entries; EXCEPTION WHEN OTHERS THEN NULL; END;
- BEGIN ALTER TABLE supplier_invoices ENABLE TRIGGER audit_supplier_invoices; EXCEPTION WHEN OTHERS THEN NULL; END;
- RAISE;
-END;
-$$;
diff --git a/supabase/migrations/20260409165300_allow_custom_first_fiscal_period_start.sql b/supabase/migrations/20260409165300_allow_custom_first_fiscal_period_start.sql
deleted file mode 100644
index c4a12652..00000000
--- a/supabase/migrations/20260409165300_allow_custom_first_fiscal_period_start.sql
+++ /dev/null
@@ -1,36 +0,0 @@
--- Allow the first fiscal period of a company to start on any day of the month,
--- per BFL 3 kap. (the first fiscal year starts on the company registration date).
--- Subsequent periods must still start on the 1st of a month.
-
--- Drop the unconditional CHECK constraint that enforces day-1 starts
-ALTER TABLE public.fiscal_periods
- DROP CONSTRAINT IF EXISTS fiscal_period_start_first_of_month;
-
--- Replace with a trigger that only enforces day-1 for non-first periods
-CREATE OR REPLACE FUNCTION enforce_first_of_month_for_subsequent_periods()
-RETURNS trigger AS $$
-BEGIN
- -- If this period starts on the 1st, no check needed
- IF EXTRACT(DAY FROM NEW.period_start) = 1 THEN
- RETURN NEW;
- END IF;
-
- -- Allow any start day only if this is the first fiscal period for the company
- IF EXISTS (
- SELECT 1 FROM public.fiscal_periods
- WHERE company_id = NEW.company_id
- AND id IS DISTINCT FROM NEW.id
- ) THEN
- RAISE EXCEPTION 'Non-first fiscal period must start on the 1st of a month';
- END IF;
-
- RETURN NEW;
-END;
-$$ LANGUAGE plpgsql;
-
-DROP TRIGGER IF EXISTS enforce_period_start_day ON public.fiscal_periods;
-
-CREATE TRIGGER enforce_period_start_day
- BEFORE INSERT OR UPDATE ON public.fiscal_periods
- FOR EACH ROW
- EXECUTE FUNCTION enforce_first_of_month_for_subsequent_periods();
diff --git a/supabase/migrations/20260411000001_user_company_ids_filter_archived.sql b/supabase/migrations/20260411000001_user_company_ids_filter_archived.sql
deleted file mode 100644
index 43384708..00000000
--- a/supabase/migrations/20260411000001_user_company_ids_filter_archived.sql
+++ /dev/null
@@ -1,38 +0,0 @@
--- Filter archived companies out of user_company_ids() helper.
---
--- Soft-deleted (archived) companies must disappear from the user's UI while
--- the underlying bookkeeping data stays intact for BFL 7 kap. 2§ 7-year
--- retention. Routing this filter through user_company_ids() makes every
--- company-scoped RLS policy honor it automatically.
---
--- Also adds companies.archived_by (who archived it) and an index on
--- archived_at IS NULL to speed up the common "list my active companies"
--- picker query.
-
-CREATE OR REPLACE FUNCTION public.user_company_ids()
-RETURNS SETOF uuid
-LANGUAGE sql
-STABLE
-SECURITY DEFINER
-SET search_path = public
-AS $$
- SELECT cm.company_id
- FROM public.company_members cm
- JOIN public.companies c ON c.id = cm.company_id
- WHERE cm.user_id = auth.uid()
- AND c.archived_at IS NULL;
-$$;
-
-GRANT EXECUTE ON FUNCTION public.user_company_ids() TO authenticated;
-
--- Track who archived a company. ON DELETE SET NULL so a future hard-purge
--- (7 years out) of the archiving user doesn't break the tombstone row.
-ALTER TABLE public.companies
- ADD COLUMN archived_by uuid REFERENCES auth.users(id) ON DELETE SET NULL;
-
--- Partial index: picker queries almost always filter to active companies.
-CREATE INDEX companies_active_idx
- ON public.companies (id)
- WHERE archived_at IS NULL;
-
-NOTIFY pgrst, 'reload schema';
diff --git a/supabase/migrations/20260411000002_profiles_anonymization.sql b/supabase/migrations/20260411000002_profiles_anonymization.sql
deleted file mode 100644
index 7bf3bc39..00000000
--- a/supabase/migrations/20260411000002_profiles_anonymization.sql
+++ /dev/null
@@ -1,13 +0,0 @@
--- Add tombstone columns to profiles for account anonymization.
---
--- When a user deletes their account we must keep the auth.users row alive
--- (otherwise ON DELETE CASCADE from companies.created_by would destroy
--- retained bookkeeping data). Instead we anonymize the profile row: strip
--- PII, stamp deleted_at + anonymized_at, and the UI falls back to
--- "Borttagen användare" wherever the profile is displayed.
---
--- email and full_name are already nullable per migration 20240101000001.
-
-ALTER TABLE public.profiles
- ADD COLUMN deleted_at timestamptz,
- ADD COLUMN anonymized_at timestamptz;
diff --git a/supabase/migrations/20260411000003_anonymize_user_account_rpc.sql b/supabase/migrations/20260411000003_anonymize_user_account_rpc.sql
deleted file mode 100644
index 086e9709..00000000
--- a/supabase/migrations/20260411000003_anonymize_user_account_rpc.sql
+++ /dev/null
@@ -1,68 +0,0 @@
--- Replace hard-delete with anonymization: retain auth.users as a tombstone.
---
--- Why: BFL 7 kap. 2§ requires räkenskapsinformation to be preserved for 7
--- years. companies.created_by references auth.users ON DELETE CASCADE, so
--- hard-deleting the user would wipe retained verifikationer. Instead we:
--- 1) verify zero owned, non-archived companies
--- 2) remove the user from all company/team memberships
--- 3) delete per-user operational state (prefs, api keys)
--- 4) anonymize the profile
--- The API route caller is responsible for banning the auth.users row via
--- the admin API afterwards (SQL can't reach supabase.auth.admin).
-
-CREATE OR REPLACE FUNCTION public.anonymize_user_account(target_user_id uuid)
-RETURNS void
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
-AS $$
-DECLARE
- blocker_count int;
-BEGIN
- IF auth.uid() IS DISTINCT FROM target_user_id THEN
- RAISE EXCEPTION 'Can only delete your own account';
- END IF;
-
- -- Precondition: zero owned, non-archived companies
- SELECT count(*) INTO blocker_count
- FROM public.company_members cm
- JOIN public.companies c ON c.id = cm.company_id
- WHERE cm.user_id = target_user_id
- AND cm.role = 'owner'
- AND c.archived_at IS NULL;
-
- IF blocker_count > 0 THEN
- RAISE EXCEPTION 'Cannot delete account: user still owns % active compan(y/ies)', blocker_count
- USING ERRCODE = 'P0001';
- END IF;
-
- -- Remove memberships
- DELETE FROM public.company_members WHERE user_id = target_user_id;
- DELETE FROM public.team_members WHERE user_id = target_user_id;
-
- -- Remove per-user operational state
- DELETE FROM public.user_preferences WHERE user_id = target_user_id;
- DELETE FROM public.api_keys WHERE user_id = target_user_id;
-
- -- Anonymize profile tombstone
- UPDATE public.profiles
- SET email = NULL,
- full_name = NULL,
- avatar_url = NULL,
- deleted_at = now(),
- anonymized_at = now(),
- updated_at = now()
- WHERE id = target_user_id;
-
- -- NOTE: auth.users row, companies.created_by FK, and audit_log.user_id
- -- are intentionally left intact. They now reference a banned, anonymized
- -- tombstone row that exists solely to keep BFL-retained data valid.
-END;
-$$;
-
-GRANT EXECUTE ON FUNCTION public.anonymize_user_account(uuid) TO authenticated;
-
--- The old delete_user_account RPC hard-deleted auth.users, which cascaded
--- away retained verifikationer via companies.created_by. It is incompatible
--- with the retention model and must be removed.
-DROP FUNCTION IF EXISTS public.delete_user_account(uuid);
diff --git a/supabase/migrations/20260411120000_active_company_rls_isolation.sql b/supabase/migrations/20260411120000_active_company_rls_isolation.sql
deleted file mode 100644
index f6b31a29..00000000
--- a/supabase/migrations/20260411120000_active_company_rls_isolation.sql
+++ /dev/null
@@ -1,664 +0,0 @@
--- =============================================================================
--- Active-company tenant isolation in RLS
--- =============================================================================
---
--- Before this migration, company-scoped RLS policies allowed access to any
--- company the user was a MEMBER of. This meant that a user with multiple
--- memberships could see cross-company data leak into dropdowns and lists
--- via direct browser-client queries (e.g. /invoices/new customer dropdown),
--- because RLS only checked membership, not which company was active.
---
--- This migration replaces `company_id IN (SELECT user_company_ids())` with
--- `company_id = current_active_company_id()` on every company-scoped policy.
--- The helper function reads `user_preferences.active_company_id` with a
--- fallback to the user's first non-archived membership. It is STABLE +
--- SECURITY DEFINER so the result is cached per query plan and it bypasses
--- RLS on the user_preferences + company_members tables it reads from.
---
--- Tables NOT updated (they stay on user_company_ids() because they must be
--- visible across all companies the user is a member of, or use a different
--- auth model entirely):
--- companies, company_members, user_preferences, profiles
--- teams, team_members, team_invitations, company_invitations
--- provider_consent_tokens, provider_otc (consent-id scoped via team_members)
--- bankid_identities (user-scoped, not company-scoped)
--- storage.objects/sie-files bucket policies (different auth surface)
---
--- =============================================================================
-
--- =============================================================================
--- 1. current_active_company_id() helper
--- =============================================================================
-CREATE OR REPLACE FUNCTION public.current_active_company_id()
-RETURNS uuid
-LANGUAGE sql
-STABLE
-SECURITY DEFINER
-SET search_path = public
-AS $$
- SELECT COALESCE(
- -- Active preference, validated against non-archived membership
- (
- SELECT up.active_company_id
- FROM public.user_preferences up
- JOIN public.company_members cm
- ON cm.user_id = up.user_id AND cm.company_id = up.active_company_id
- JOIN public.companies c
- ON c.id = cm.company_id AND c.archived_at IS NULL
- WHERE up.user_id = auth.uid()
- ),
- -- Fallback: first non-archived membership by created_at
- (
- SELECT cm.company_id
- FROM public.company_members cm
- JOIN public.companies c
- ON c.id = cm.company_id AND c.archived_at IS NULL
- WHERE cm.user_id = auth.uid()
- ORDER BY cm.created_at ASC
- LIMIT 1
- )
- );
-$$;
-
-GRANT EXECUTE ON FUNCTION public.current_active_company_id() TO authenticated;
-
-COMMENT ON FUNCTION public.current_active_company_id() IS
- 'Returns the active company id for auth.uid(), reading user_preferences.active_company_id '
- 'with validation against non-archived company_members, and falling back to the user''s '
- 'first non-archived membership. Used by RLS policies to enforce single-active-company '
- 'tenant isolation. STABLE so the result is cached per query plan.';
-
--- =============================================================================
--- 2. Drop old policies on company-scoped data tables
--- =============================================================================
--- We enumerate every company-scoped data table that currently uses
--- user_company_ids() and drop all of its policies so we can recreate them
--- below with current_active_company_id().
-
-DO $$
-DECLARE
- pol RECORD;
- affected_tables TEXT[] := ARRAY[
- 'company_settings', 'chart_of_accounts', 'fiscal_periods',
- 'journal_entries', 'journal_entry_lines',
- 'account_balances', 'voucher_sequences',
- 'transactions', 'bank_connections', 'bank_file_imports',
- 'customers', 'invoices', 'invoice_items',
- 'invoice_reminders', 'invoice_payments',
- 'suppliers', 'supplier_invoices', 'supplier_invoice_items',
- 'supplier_invoice_payments',
- 'receipts', 'receipt_line_items',
- 'document_attachments', 'invoice_inbox_items',
- 'mapping_rules', 'categorization_templates',
- 'deadlines', 'cost_centers', 'projects',
- 'salary_payments', 'mileage_entries',
- 'sie_imports', 'sie_account_mappings',
- 'calendar_feeds', 'chat_sessions', 'chat_messages',
- 'ai_usage_tracking', 'extension_data', 'api_keys',
- 'skatteverket_tokens', 'pending_operations', 'payment_match_log',
- 'event_log', 'notification_log', 'audit_log',
- 'provider_consents', 'voucher_gap_explanations',
- 'automation_webhooks', 'email_connections'
- ];
- tbl TEXT;
-BEGIN
- FOREACH tbl IN ARRAY affected_tables
- LOOP
- IF EXISTS (
- SELECT 1 FROM information_schema.tables
- WHERE table_schema = 'public' AND table_name = tbl
- ) THEN
- FOR pol IN
- SELECT policyname FROM pg_policies
- WHERE schemaname = 'public' AND tablename = tbl
- LOOP
- EXECUTE format('DROP POLICY IF EXISTS %I ON public.%I', pol.policyname, tbl);
- END LOOP;
- END IF;
- END LOOP;
-END $$;
-
--- =============================================================================
--- 3. Recreate policies using current_active_company_id()
--- =============================================================================
--- Same shape as the previous user_company_ids()-based policies, just
--- swapping the membership IN clause for an equality check against the
--- single active company.
-
--- company_settings
-CREATE POLICY "company_settings_select" ON public.company_settings
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "company_settings_insert" ON public.company_settings
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "company_settings_update" ON public.company_settings
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "company_settings_delete" ON public.company_settings
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- chart_of_accounts
-CREATE POLICY "chart_of_accounts_select" ON public.chart_of_accounts
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "chart_of_accounts_insert" ON public.chart_of_accounts
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "chart_of_accounts_update" ON public.chart_of_accounts
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "chart_of_accounts_delete" ON public.chart_of_accounts
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- fiscal_periods
-CREATE POLICY "fiscal_periods_select" ON public.fiscal_periods
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "fiscal_periods_insert" ON public.fiscal_periods
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "fiscal_periods_update" ON public.fiscal_periods
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "fiscal_periods_delete" ON public.fiscal_periods
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- journal_entries
-CREATE POLICY "journal_entries_select" ON public.journal_entries
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "journal_entries_insert" ON public.journal_entries
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "journal_entries_update" ON public.journal_entries
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "journal_entries_delete" ON public.journal_entries
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- journal_entry_lines (child: join to parent journal_entries)
-CREATE POLICY "journal_entry_lines_select" ON public.journal_entry_lines
- FOR SELECT USING (
- EXISTS (SELECT 1 FROM public.journal_entries je
- WHERE je.id = journal_entry_lines.journal_entry_id
- AND je.company_id = public.current_active_company_id())
- );
-CREATE POLICY "journal_entry_lines_insert" ON public.journal_entry_lines
- FOR INSERT WITH CHECK (
- EXISTS (SELECT 1 FROM public.journal_entries je
- WHERE je.id = journal_entry_lines.journal_entry_id
- AND je.company_id = public.current_active_company_id())
- );
-CREATE POLICY "journal_entry_lines_update" ON public.journal_entry_lines
- FOR UPDATE USING (
- EXISTS (SELECT 1 FROM public.journal_entries je
- WHERE je.id = journal_entry_lines.journal_entry_id
- AND je.company_id = public.current_active_company_id())
- );
-CREATE POLICY "journal_entry_lines_delete" ON public.journal_entry_lines
- FOR DELETE USING (
- EXISTS (SELECT 1 FROM public.journal_entries je
- WHERE je.id = journal_entry_lines.journal_entry_id
- AND je.company_id = public.current_active_company_id())
- );
-
--- account_balances (may not exist on fresh DBs)
-DO $$ BEGIN
- IF EXISTS (
- SELECT 1 FROM information_schema.tables
- WHERE table_schema = 'public' AND table_name = 'account_balances'
- ) THEN
- EXECUTE 'CREATE POLICY "account_balances_select" ON public.account_balances FOR SELECT USING (company_id = public.current_active_company_id())';
- EXECUTE 'CREATE POLICY "account_balances_insert" ON public.account_balances FOR INSERT WITH CHECK (company_id = public.current_active_company_id())';
- EXECUTE 'CREATE POLICY "account_balances_update" ON public.account_balances FOR UPDATE USING (company_id = public.current_active_company_id())';
- EXECUTE 'CREATE POLICY "account_balances_delete" ON public.account_balances FOR DELETE USING (company_id = public.current_active_company_id())';
- END IF;
-END $$;
-
--- voucher_sequences
-CREATE POLICY "voucher_sequences_select" ON public.voucher_sequences
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "voucher_sequences_insert" ON public.voucher_sequences
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "voucher_sequences_update" ON public.voucher_sequences
- FOR UPDATE USING (company_id = public.current_active_company_id());
-
--- transactions
-CREATE POLICY "transactions_select" ON public.transactions
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "transactions_insert" ON public.transactions
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "transactions_update" ON public.transactions
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "transactions_delete" ON public.transactions
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- bank_connections
-CREATE POLICY "bank_connections_select" ON public.bank_connections
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "bank_connections_insert" ON public.bank_connections
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "bank_connections_update" ON public.bank_connections
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "bank_connections_delete" ON public.bank_connections
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- bank_file_imports
-CREATE POLICY "bank_file_imports_select" ON public.bank_file_imports
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "bank_file_imports_insert" ON public.bank_file_imports
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "bank_file_imports_update" ON public.bank_file_imports
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "bank_file_imports_delete" ON public.bank_file_imports
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- customers
-CREATE POLICY "customers_select" ON public.customers
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "customers_insert" ON public.customers
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "customers_update" ON public.customers
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "customers_delete" ON public.customers
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- invoices
-CREATE POLICY "invoices_select" ON public.invoices
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "invoices_insert" ON public.invoices
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "invoices_update" ON public.invoices
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "invoices_delete" ON public.invoices
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- invoice_items (child: join to parent invoices)
-CREATE POLICY "invoice_items_select" ON public.invoice_items
- FOR SELECT USING (
- EXISTS (SELECT 1 FROM public.invoices i
- WHERE i.id = invoice_items.invoice_id
- AND i.company_id = public.current_active_company_id())
- );
-CREATE POLICY "invoice_items_insert" ON public.invoice_items
- FOR INSERT WITH CHECK (
- EXISTS (SELECT 1 FROM public.invoices i
- WHERE i.id = invoice_items.invoice_id
- AND i.company_id = public.current_active_company_id())
- );
-CREATE POLICY "invoice_items_update" ON public.invoice_items
- FOR UPDATE USING (
- EXISTS (SELECT 1 FROM public.invoices i
- WHERE i.id = invoice_items.invoice_id
- AND i.company_id = public.current_active_company_id())
- );
-CREATE POLICY "invoice_items_delete" ON public.invoice_items
- FOR DELETE USING (
- EXISTS (SELECT 1 FROM public.invoices i
- WHERE i.id = invoice_items.invoice_id
- AND i.company_id = public.current_active_company_id())
- );
-
--- invoice_reminders
-CREATE POLICY "invoice_reminders_select" ON public.invoice_reminders
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "invoice_reminders_insert" ON public.invoice_reminders
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "invoice_reminders_update" ON public.invoice_reminders
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "invoice_reminders_delete" ON public.invoice_reminders
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- invoice_payments
-CREATE POLICY "invoice_payments_select" ON public.invoice_payments
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "invoice_payments_insert" ON public.invoice_payments
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "invoice_payments_update" ON public.invoice_payments
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "invoice_payments_delete" ON public.invoice_payments
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- suppliers
-CREATE POLICY "suppliers_select" ON public.suppliers
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "suppliers_insert" ON public.suppliers
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "suppliers_update" ON public.suppliers
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "suppliers_delete" ON public.suppliers
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- supplier_invoices
-CREATE POLICY "supplier_invoices_select" ON public.supplier_invoices
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "supplier_invoices_insert" ON public.supplier_invoices
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "supplier_invoices_update" ON public.supplier_invoices
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "supplier_invoices_delete" ON public.supplier_invoices
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- supplier_invoice_items (child: join to parent supplier_invoices)
-CREATE POLICY "supplier_invoice_items_select" ON public.supplier_invoice_items
- FOR SELECT USING (
- EXISTS (SELECT 1 FROM public.supplier_invoices si
- WHERE si.id = supplier_invoice_items.supplier_invoice_id
- AND si.company_id = public.current_active_company_id())
- );
-CREATE POLICY "supplier_invoice_items_insert" ON public.supplier_invoice_items
- FOR INSERT WITH CHECK (
- EXISTS (SELECT 1 FROM public.supplier_invoices si
- WHERE si.id = supplier_invoice_items.supplier_invoice_id
- AND si.company_id = public.current_active_company_id())
- );
-CREATE POLICY "supplier_invoice_items_update" ON public.supplier_invoice_items
- FOR UPDATE USING (
- EXISTS (SELECT 1 FROM public.supplier_invoices si
- WHERE si.id = supplier_invoice_items.supplier_invoice_id
- AND si.company_id = public.current_active_company_id())
- );
-CREATE POLICY "supplier_invoice_items_delete" ON public.supplier_invoice_items
- FOR DELETE USING (
- EXISTS (SELECT 1 FROM public.supplier_invoices si
- WHERE si.id = supplier_invoice_items.supplier_invoice_id
- AND si.company_id = public.current_active_company_id())
- );
-
--- supplier_invoice_payments
-CREATE POLICY "supplier_invoice_payments_select" ON public.supplier_invoice_payments
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "supplier_invoice_payments_insert" ON public.supplier_invoice_payments
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "supplier_invoice_payments_update" ON public.supplier_invoice_payments
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "supplier_invoice_payments_delete" ON public.supplier_invoice_payments
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- receipts
-CREATE POLICY "receipts_select" ON public.receipts
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "receipts_insert" ON public.receipts
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "receipts_update" ON public.receipts
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "receipts_delete" ON public.receipts
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- receipt_line_items (child: join to parent receipts)
-CREATE POLICY "receipt_line_items_select" ON public.receipt_line_items
- FOR SELECT USING (
- EXISTS (SELECT 1 FROM public.receipts r
- WHERE r.id = receipt_line_items.receipt_id
- AND r.company_id = public.current_active_company_id())
- );
-CREATE POLICY "receipt_line_items_insert" ON public.receipt_line_items
- FOR INSERT WITH CHECK (
- EXISTS (SELECT 1 FROM public.receipts r
- WHERE r.id = receipt_line_items.receipt_id
- AND r.company_id = public.current_active_company_id())
- );
-CREATE POLICY "receipt_line_items_update" ON public.receipt_line_items
- FOR UPDATE USING (
- EXISTS (SELECT 1 FROM public.receipts r
- WHERE r.id = receipt_line_items.receipt_id
- AND r.company_id = public.current_active_company_id())
- );
-CREATE POLICY "receipt_line_items_delete" ON public.receipt_line_items
- FOR DELETE USING (
- EXISTS (SELECT 1 FROM public.receipts r
- WHERE r.id = receipt_line_items.receipt_id
- AND r.company_id = public.current_active_company_id())
- );
-
--- document_attachments (DELETE is blocked by block_document_deletion trigger
--- but we still add a policy for completeness — the trigger runs after RLS)
-CREATE POLICY "document_attachments_select" ON public.document_attachments
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "document_attachments_insert" ON public.document_attachments
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "document_attachments_update" ON public.document_attachments
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "document_attachments_delete" ON public.document_attachments
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- invoice_inbox_items
-CREATE POLICY "invoice_inbox_items_select" ON public.invoice_inbox_items
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "invoice_inbox_items_insert" ON public.invoice_inbox_items
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "invoice_inbox_items_update" ON public.invoice_inbox_items
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "invoice_inbox_items_delete" ON public.invoice_inbox_items
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- mapping_rules (system rules have company_id IS NULL)
-CREATE POLICY "mapping_rules_select" ON public.mapping_rules
- FOR SELECT USING (company_id = public.current_active_company_id() OR company_id IS NULL);
-CREATE POLICY "mapping_rules_insert" ON public.mapping_rules
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "mapping_rules_update" ON public.mapping_rules
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "mapping_rules_delete" ON public.mapping_rules
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- categorization_templates
-CREATE POLICY "categorization_templates_select" ON public.categorization_templates
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "categorization_templates_insert" ON public.categorization_templates
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "categorization_templates_update" ON public.categorization_templates
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "categorization_templates_delete" ON public.categorization_templates
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- deadlines
-CREATE POLICY "deadlines_select" ON public.deadlines
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "deadlines_insert" ON public.deadlines
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "deadlines_update" ON public.deadlines
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "deadlines_delete" ON public.deadlines
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- cost_centers
-CREATE POLICY "cost_centers_select" ON public.cost_centers
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "cost_centers_insert" ON public.cost_centers
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "cost_centers_update" ON public.cost_centers
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "cost_centers_delete" ON public.cost_centers
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- projects
-CREATE POLICY "projects_select" ON public.projects
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "projects_insert" ON public.projects
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "projects_update" ON public.projects
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "projects_delete" ON public.projects
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- salary_payments (may not exist on fresh DBs)
-DO $$ BEGIN
- IF EXISTS (
- SELECT 1 FROM information_schema.tables
- WHERE table_schema = 'public' AND table_name = 'salary_payments'
- ) THEN
- EXECUTE 'CREATE POLICY "salary_payments_select" ON public.salary_payments FOR SELECT USING (company_id = public.current_active_company_id())';
- EXECUTE 'CREATE POLICY "salary_payments_insert" ON public.salary_payments FOR INSERT WITH CHECK (company_id = public.current_active_company_id())';
- EXECUTE 'CREATE POLICY "salary_payments_update" ON public.salary_payments FOR UPDATE USING (company_id = public.current_active_company_id())';
- EXECUTE 'CREATE POLICY "salary_payments_delete" ON public.salary_payments FOR DELETE USING (company_id = public.current_active_company_id())';
- END IF;
-END $$;
-
--- mileage_entries (may not exist on fresh DBs)
-DO $$ BEGIN
- IF EXISTS (
- SELECT 1 FROM information_schema.tables
- WHERE table_schema = 'public' AND table_name = 'mileage_entries'
- ) THEN
- EXECUTE 'CREATE POLICY "mileage_entries_select" ON public.mileage_entries FOR SELECT USING (company_id = public.current_active_company_id())';
- EXECUTE 'CREATE POLICY "mileage_entries_insert" ON public.mileage_entries FOR INSERT WITH CHECK (company_id = public.current_active_company_id())';
- EXECUTE 'CREATE POLICY "mileage_entries_update" ON public.mileage_entries FOR UPDATE USING (company_id = public.current_active_company_id())';
- EXECUTE 'CREATE POLICY "mileage_entries_delete" ON public.mileage_entries FOR DELETE USING (company_id = public.current_active_company_id())';
- END IF;
-END $$;
-
--- sie_imports
-CREATE POLICY "sie_imports_select" ON public.sie_imports
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "sie_imports_insert" ON public.sie_imports
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "sie_imports_update" ON public.sie_imports
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "sie_imports_delete" ON public.sie_imports
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- sie_account_mappings
-CREATE POLICY "sie_account_mappings_select" ON public.sie_account_mappings
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "sie_account_mappings_insert" ON public.sie_account_mappings
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "sie_account_mappings_update" ON public.sie_account_mappings
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "sie_account_mappings_delete" ON public.sie_account_mappings
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- calendar_feeds
-CREATE POLICY "calendar_feeds_select" ON public.calendar_feeds
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "calendar_feeds_insert" ON public.calendar_feeds
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "calendar_feeds_update" ON public.calendar_feeds
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "calendar_feeds_delete" ON public.calendar_feeds
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- chat_sessions
-CREATE POLICY "chat_sessions_select" ON public.chat_sessions
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "chat_sessions_insert" ON public.chat_sessions
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "chat_sessions_update" ON public.chat_sessions
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "chat_sessions_delete" ON public.chat_sessions
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- chat_messages
-CREATE POLICY "chat_messages_select" ON public.chat_messages
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "chat_messages_insert" ON public.chat_messages
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "chat_messages_update" ON public.chat_messages
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "chat_messages_delete" ON public.chat_messages
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- ai_usage_tracking (SELECT + INSERT only — no UPDATE/DELETE policy)
-CREATE POLICY "ai_usage_tracking_select" ON public.ai_usage_tracking
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "ai_usage_tracking_insert" ON public.ai_usage_tracking
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-
--- extension_data
-CREATE POLICY "extension_data_select" ON public.extension_data
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "extension_data_insert" ON public.extension_data
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "extension_data_update" ON public.extension_data
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "extension_data_delete" ON public.extension_data
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- api_keys
-CREATE POLICY "api_keys_select" ON public.api_keys
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "api_keys_insert" ON public.api_keys
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "api_keys_update" ON public.api_keys
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "api_keys_delete" ON public.api_keys
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- skatteverket_tokens
-CREATE POLICY "skatteverket_tokens_select" ON public.skatteverket_tokens
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "skatteverket_tokens_insert" ON public.skatteverket_tokens
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "skatteverket_tokens_update" ON public.skatteverket_tokens
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "skatteverket_tokens_delete" ON public.skatteverket_tokens
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- pending_operations (SELECT + UPDATE only — inserts via service role)
-CREATE POLICY "pending_operations_select" ON public.pending_operations
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "pending_operations_update" ON public.pending_operations
- FOR UPDATE USING (company_id = public.current_active_company_id());
-
--- payment_match_log (nullable company_id)
-CREATE POLICY "payment_match_log_select" ON public.payment_match_log
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "payment_match_log_insert" ON public.payment_match_log
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id() OR company_id IS NULL);
-
--- event_log (SELECT only for users — writes via handler in service role)
-CREATE POLICY "event_log_select" ON public.event_log
- FOR SELECT USING (company_id = public.current_active_company_id());
-
--- notification_log (nullable company_id for system-wide notifications)
-CREATE POLICY "notification_log_select" ON public.notification_log
- FOR SELECT USING (company_id = public.current_active_company_id() OR company_id IS NULL);
-
--- audit_log (SELECT only for users — writes via SECURITY DEFINER triggers)
-CREATE POLICY "audit_log_select" ON public.audit_log
- FOR SELECT USING (company_id = public.current_active_company_id());
-
--- provider_consents
-CREATE POLICY "provider_consents_select" ON public.provider_consents
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "provider_consents_insert" ON public.provider_consents
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "provider_consents_update" ON public.provider_consents
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "provider_consents_delete" ON public.provider_consents
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- voucher_gap_explanations (preserves the owner/admin team_members check)
-CREATE POLICY "voucher_gap_explanations_select" ON public.voucher_gap_explanations
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "voucher_gap_explanations_insert" ON public.voucher_gap_explanations
- FOR INSERT WITH CHECK (
- company_id = public.current_active_company_id()
- AND EXISTS (
- SELECT 1 FROM public.team_members tm
- JOIN public.companies c ON c.team_id = tm.team_id
- WHERE c.id = company_id
- AND tm.user_id = auth.uid()
- AND tm.role IN ('owner', 'admin')
- )
- );
-CREATE POLICY "voucher_gap_explanations_update" ON public.voucher_gap_explanations
- FOR UPDATE USING (
- company_id = public.current_active_company_id()
- AND EXISTS (
- SELECT 1 FROM public.team_members tm
- JOIN public.companies c ON c.team_id = tm.team_id
- WHERE c.id = company_id
- AND tm.user_id = auth.uid()
- AND tm.role IN ('owner', 'admin')
- )
- );
-
--- automation_webhooks
-CREATE POLICY "automation_webhooks_select" ON public.automation_webhooks
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "automation_webhooks_insert" ON public.automation_webhooks
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "automation_webhooks_update" ON public.automation_webhooks
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "automation_webhooks_delete" ON public.automation_webhooks
- FOR DELETE USING (company_id = public.current_active_company_id());
-
--- email_connections
-CREATE POLICY "email_connections_select" ON public.email_connections
- FOR SELECT USING (company_id = public.current_active_company_id());
-CREATE POLICY "email_connections_insert" ON public.email_connections
- FOR INSERT WITH CHECK (company_id = public.current_active_company_id());
-CREATE POLICY "email_connections_update" ON public.email_connections
- FOR UPDATE USING (company_id = public.current_active_company_id());
-CREATE POLICY "email_connections_delete" ON public.email_connections
- FOR DELETE USING (company_id = public.current_active_company_id());
diff --git a/supabase/migrations/20260411130000_viewer_role_read_only.sql b/supabase/migrations/20260411130000_viewer_role_read_only.sql
deleted file mode 100644
index c28dd5a5..00000000
--- a/supabase/migrations/20260411130000_viewer_role_read_only.sql
+++ /dev/null
@@ -1,364 +0,0 @@
--- =============================================================================
--- Viewer role read-only enforcement
--- =============================================================================
---
--- Activates the `viewer` role in company_members. Until now the role existed
--- in the type system (CompanyRole) but was dormant — a viewer had the same
--- write capabilities as a member. This migration makes viewers truly
--- read-only at the database level.
---
--- Mechanism:
--- 1. New helper `public.current_user_can_write()` returns true only if
--- the authenticated user's role in the active company is NOT 'viewer'.
--- 2. Every INSERT / UPDATE / DELETE policy on company-scoped tables is
--- recreated with an added `AND public.current_user_can_write()` clause.
--- 3. SELECT policies are left untouched — viewers read everything.
---
--- After this migration, any write by a viewer is rejected with:
--- "new row violates row-level security policy" (server-side)
--- regardless of whether it originates from the UI, a browser console, or
--- an API key bound to a viewer's session.
--- =============================================================================
-
--- =============================================================================
--- 1. current_user_can_write() helper
--- =============================================================================
-CREATE OR REPLACE FUNCTION public.current_user_can_write()
-RETURNS boolean
-LANGUAGE sql
-STABLE
-SECURITY DEFINER
-SET search_path = public
-AS $$
- SELECT EXISTS (
- SELECT 1
- FROM public.company_members cm
- WHERE cm.user_id = auth.uid()
- AND cm.company_id = public.current_active_company_id()
- AND cm.role <> 'viewer'
- );
-$$;
-
-GRANT EXECUTE ON FUNCTION public.current_user_can_write() TO authenticated;
-
-COMMENT ON FUNCTION public.current_user_can_write() IS
- 'Returns true if auth.uid() has a non-viewer role (owner / admin / member) '
- 'in the current active company. Used by RLS INSERT/UPDATE/DELETE policies '
- 'to make the ''viewer'' role truly read-only. Viewers, non-members, and '
- 'unauthenticated callers all get false.';
-
--- =============================================================================
--- 2. Standard tables — recreate INSERT / UPDATE / DELETE policies
--- =============================================================================
--- Loop over all company-scoped tables that currently have the full set of
--- insert/update/delete policies. SELECT policies are left alone (viewers
--- must be able to read everything).
-
-DO $$
-DECLARE
- t TEXT;
- standard_tables TEXT[] := ARRAY[
- 'company_settings', 'chart_of_accounts', 'fiscal_periods', 'journal_entries',
- 'transactions', 'bank_connections', 'bank_file_imports',
- 'customers', 'invoices', 'invoice_reminders', 'invoice_payments',
- 'suppliers', 'supplier_invoices', 'supplier_invoice_payments',
- 'receipts', 'document_attachments', 'invoice_inbox_items',
- 'categorization_templates', 'deadlines', 'cost_centers', 'projects',
- 'sie_imports', 'sie_account_mappings', 'calendar_feeds',
- 'chat_sessions', 'chat_messages',
- 'extension_data', 'api_keys', 'skatteverket_tokens',
- 'provider_consents', 'automation_webhooks', 'email_connections'
- ];
-BEGIN
- FOREACH t IN ARRAY standard_tables
- LOOP
- IF EXISTS (
- SELECT 1 FROM information_schema.tables
- WHERE table_schema = 'public' AND table_name = t
- ) THEN
- EXECUTE format('DROP POLICY IF EXISTS "%s_insert" ON public.%I', t, t);
- EXECUTE format('DROP POLICY IF EXISTS "%s_update" ON public.%I', t, t);
- EXECUTE format('DROP POLICY IF EXISTS "%s_delete" ON public.%I', t, t);
-
- EXECUTE format(
- 'CREATE POLICY "%s_insert" ON public.%I '
- 'FOR INSERT WITH CHECK ('
- 'company_id = public.current_active_company_id() '
- 'AND public.current_user_can_write())',
- t, t
- );
- EXECUTE format(
- 'CREATE POLICY "%s_update" ON public.%I '
- 'FOR UPDATE USING ('
- 'company_id = public.current_active_company_id() '
- 'AND public.current_user_can_write())',
- t, t
- );
- EXECUTE format(
- 'CREATE POLICY "%s_delete" ON public.%I '
- 'FOR DELETE USING ('
- 'company_id = public.current_active_company_id() '
- 'AND public.current_user_can_write())',
- t, t
- );
- END IF;
- END LOOP;
-END $$;
-
--- =============================================================================
--- 3. voucher_sequences (INSERT + UPDATE only, no DELETE policy)
--- =============================================================================
-DROP POLICY IF EXISTS "voucher_sequences_insert" ON public.voucher_sequences;
-DROP POLICY IF EXISTS "voucher_sequences_update" ON public.voucher_sequences;
-CREATE POLICY "voucher_sequences_insert" ON public.voucher_sequences
- FOR INSERT WITH CHECK (
- company_id = public.current_active_company_id()
- AND public.current_user_can_write()
- );
-CREATE POLICY "voucher_sequences_update" ON public.voucher_sequences
- FOR UPDATE USING (
- company_id = public.current_active_company_id()
- AND public.current_user_can_write()
- );
-
--- =============================================================================
--- 4. mapping_rules (standard, but with OR company_id IS NULL on SELECT only)
--- =============================================================================
--- The SELECT policy keeps its OR company_id IS NULL exception for system
--- rules — viewers can still read them. Writes drop the NULL branch because
--- viewers never create rules (and non-viewers only create company-scoped
--- rules, not system rules).
-DROP POLICY IF EXISTS "mapping_rules_insert" ON public.mapping_rules;
-DROP POLICY IF EXISTS "mapping_rules_update" ON public.mapping_rules;
-DROP POLICY IF EXISTS "mapping_rules_delete" ON public.mapping_rules;
-CREATE POLICY "mapping_rules_insert" ON public.mapping_rules
- FOR INSERT WITH CHECK (
- company_id = public.current_active_company_id()
- AND public.current_user_can_write()
- );
-CREATE POLICY "mapping_rules_update" ON public.mapping_rules
- FOR UPDATE USING (
- company_id = public.current_active_company_id()
- AND public.current_user_can_write()
- );
-CREATE POLICY "mapping_rules_delete" ON public.mapping_rules
- FOR DELETE USING (
- company_id = public.current_active_company_id()
- AND public.current_user_can_write()
- );
-
--- =============================================================================
--- 5. ai_usage_tracking (INSERT only)
--- =============================================================================
-DROP POLICY IF EXISTS "ai_usage_tracking_insert" ON public.ai_usage_tracking;
-CREATE POLICY "ai_usage_tracking_insert" ON public.ai_usage_tracking
- FOR INSERT WITH CHECK (
- company_id = public.current_active_company_id()
- AND public.current_user_can_write()
- );
-
--- =============================================================================
--- 6. pending_operations (UPDATE only — inserts via service role)
--- =============================================================================
-DROP POLICY IF EXISTS "pending_operations_update" ON public.pending_operations;
-CREATE POLICY "pending_operations_update" ON public.pending_operations
- FOR UPDATE USING (
- company_id = public.current_active_company_id()
- AND public.current_user_can_write()
- );
-
--- =============================================================================
--- 7. payment_match_log (INSERT only, nullable company_id)
--- =============================================================================
-DROP POLICY IF EXISTS "payment_match_log_insert" ON public.payment_match_log;
-CREATE POLICY "payment_match_log_insert" ON public.payment_match_log
- FOR INSERT WITH CHECK (
- (company_id = public.current_active_company_id() OR company_id IS NULL)
- AND public.current_user_can_write()
- );
-
--- =============================================================================
--- 8. Child tables — join to parent via EXISTS
--- =============================================================================
--- journal_entry_lines
-DROP POLICY IF EXISTS "journal_entry_lines_insert" ON public.journal_entry_lines;
-DROP POLICY IF EXISTS "journal_entry_lines_update" ON public.journal_entry_lines;
-DROP POLICY IF EXISTS "journal_entry_lines_delete" ON public.journal_entry_lines;
-CREATE POLICY "journal_entry_lines_insert" ON public.journal_entry_lines
- FOR INSERT WITH CHECK (
- EXISTS (SELECT 1 FROM public.journal_entries je
- WHERE je.id = journal_entry_lines.journal_entry_id
- AND je.company_id = public.current_active_company_id())
- AND public.current_user_can_write()
- );
-CREATE POLICY "journal_entry_lines_update" ON public.journal_entry_lines
- FOR UPDATE USING (
- EXISTS (SELECT 1 FROM public.journal_entries je
- WHERE je.id = journal_entry_lines.journal_entry_id
- AND je.company_id = public.current_active_company_id())
- AND public.current_user_can_write()
- );
-CREATE POLICY "journal_entry_lines_delete" ON public.journal_entry_lines
- FOR DELETE USING (
- EXISTS (SELECT 1 FROM public.journal_entries je
- WHERE je.id = journal_entry_lines.journal_entry_id
- AND je.company_id = public.current_active_company_id())
- AND public.current_user_can_write()
- );
-
--- invoice_items
-DROP POLICY IF EXISTS "invoice_items_insert" ON public.invoice_items;
-DROP POLICY IF EXISTS "invoice_items_update" ON public.invoice_items;
-DROP POLICY IF EXISTS "invoice_items_delete" ON public.invoice_items;
-CREATE POLICY "invoice_items_insert" ON public.invoice_items
- FOR INSERT WITH CHECK (
- EXISTS (SELECT 1 FROM public.invoices i
- WHERE i.id = invoice_items.invoice_id
- AND i.company_id = public.current_active_company_id())
- AND public.current_user_can_write()
- );
-CREATE POLICY "invoice_items_update" ON public.invoice_items
- FOR UPDATE USING (
- EXISTS (SELECT 1 FROM public.invoices i
- WHERE i.id = invoice_items.invoice_id
- AND i.company_id = public.current_active_company_id())
- AND public.current_user_can_write()
- );
-CREATE POLICY "invoice_items_delete" ON public.invoice_items
- FOR DELETE USING (
- EXISTS (SELECT 1 FROM public.invoices i
- WHERE i.id = invoice_items.invoice_id
- AND i.company_id = public.current_active_company_id())
- AND public.current_user_can_write()
- );
-
--- supplier_invoice_items
-DROP POLICY IF EXISTS "supplier_invoice_items_insert" ON public.supplier_invoice_items;
-DROP POLICY IF EXISTS "supplier_invoice_items_update" ON public.supplier_invoice_items;
-DROP POLICY IF EXISTS "supplier_invoice_items_delete" ON public.supplier_invoice_items;
-CREATE POLICY "supplier_invoice_items_insert" ON public.supplier_invoice_items
- FOR INSERT WITH CHECK (
- EXISTS (SELECT 1 FROM public.supplier_invoices si
- WHERE si.id = supplier_invoice_items.supplier_invoice_id
- AND si.company_id = public.current_active_company_id())
- AND public.current_user_can_write()
- );
-CREATE POLICY "supplier_invoice_items_update" ON public.supplier_invoice_items
- FOR UPDATE USING (
- EXISTS (SELECT 1 FROM public.supplier_invoices si
- WHERE si.id = supplier_invoice_items.supplier_invoice_id
- AND si.company_id = public.current_active_company_id())
- AND public.current_user_can_write()
- );
-CREATE POLICY "supplier_invoice_items_delete" ON public.supplier_invoice_items
- FOR DELETE USING (
- EXISTS (SELECT 1 FROM public.supplier_invoices si
- WHERE si.id = supplier_invoice_items.supplier_invoice_id
- AND si.company_id = public.current_active_company_id())
- AND public.current_user_can_write()
- );
-
--- receipt_line_items
-DROP POLICY IF EXISTS "receipt_line_items_insert" ON public.receipt_line_items;
-DROP POLICY IF EXISTS "receipt_line_items_update" ON public.receipt_line_items;
-DROP POLICY IF EXISTS "receipt_line_items_delete" ON public.receipt_line_items;
-CREATE POLICY "receipt_line_items_insert" ON public.receipt_line_items
- FOR INSERT WITH CHECK (
- EXISTS (SELECT 1 FROM public.receipts r
- WHERE r.id = receipt_line_items.receipt_id
- AND r.company_id = public.current_active_company_id())
- AND public.current_user_can_write()
- );
-CREATE POLICY "receipt_line_items_update" ON public.receipt_line_items
- FOR UPDATE USING (
- EXISTS (SELECT 1 FROM public.receipts r
- WHERE r.id = receipt_line_items.receipt_id
- AND r.company_id = public.current_active_company_id())
- AND public.current_user_can_write()
- );
-CREATE POLICY "receipt_line_items_delete" ON public.receipt_line_items
- FOR DELETE USING (
- EXISTS (SELECT 1 FROM public.receipts r
- WHERE r.id = receipt_line_items.receipt_id
- AND r.company_id = public.current_active_company_id())
- AND public.current_user_can_write()
- );
-
--- =============================================================================
--- 9. voucher_gap_explanations (preserves owner/admin team_members check)
--- =============================================================================
--- The existing policy already blocks non-(owner|admin) via team_members, so
--- viewers are de facto blocked. We add current_user_can_write() as a
--- defensive second layer so a future refactor of team_members can't
--- accidentally re-open viewer writes.
-DROP POLICY IF EXISTS "voucher_gap_explanations_insert" ON public.voucher_gap_explanations;
-DROP POLICY IF EXISTS "voucher_gap_explanations_update" ON public.voucher_gap_explanations;
-CREATE POLICY "voucher_gap_explanations_insert" ON public.voucher_gap_explanations
- FOR INSERT WITH CHECK (
- company_id = public.current_active_company_id()
- AND public.current_user_can_write()
- AND EXISTS (
- SELECT 1 FROM public.team_members tm
- JOIN public.companies c ON c.team_id = tm.team_id
- WHERE c.id = company_id
- AND tm.user_id = auth.uid()
- AND tm.role IN ('owner', 'admin')
- )
- );
-CREATE POLICY "voucher_gap_explanations_update" ON public.voucher_gap_explanations
- FOR UPDATE USING (
- company_id = public.current_active_company_id()
- AND public.current_user_can_write()
- AND EXISTS (
- SELECT 1 FROM public.team_members tm
- JOIN public.companies c ON c.team_id = tm.team_id
- WHERE c.id = company_id
- AND tm.user_id = auth.uid()
- AND tm.role IN ('owner', 'admin')
- )
- );
-
--- =============================================================================
--- 10. Conditional tables (may not exist on fresh DBs)
--- =============================================================================
-DO $$ BEGIN
- IF EXISTS (
- SELECT 1 FROM information_schema.tables
- WHERE table_schema = 'public' AND table_name = 'account_balances'
- ) THEN
- EXECUTE 'DROP POLICY IF EXISTS "account_balances_insert" ON public.account_balances';
- EXECUTE 'DROP POLICY IF EXISTS "account_balances_update" ON public.account_balances';
- EXECUTE 'DROP POLICY IF EXISTS "account_balances_delete" ON public.account_balances';
- EXECUTE 'CREATE POLICY "account_balances_insert" ON public.account_balances FOR INSERT WITH CHECK (company_id = public.current_active_company_id() AND public.current_user_can_write())';
- EXECUTE 'CREATE POLICY "account_balances_update" ON public.account_balances FOR UPDATE USING (company_id = public.current_active_company_id() AND public.current_user_can_write())';
- EXECUTE 'CREATE POLICY "account_balances_delete" ON public.account_balances FOR DELETE USING (company_id = public.current_active_company_id() AND public.current_user_can_write())';
- END IF;
-END $$;
-
-DO $$ BEGIN
- IF EXISTS (
- SELECT 1 FROM information_schema.tables
- WHERE table_schema = 'public' AND table_name = 'salary_payments'
- ) THEN
- EXECUTE 'DROP POLICY IF EXISTS "salary_payments_insert" ON public.salary_payments';
- EXECUTE 'DROP POLICY IF EXISTS "salary_payments_update" ON public.salary_payments';
- EXECUTE 'DROP POLICY IF EXISTS "salary_payments_delete" ON public.salary_payments';
- EXECUTE 'CREATE POLICY "salary_payments_insert" ON public.salary_payments FOR INSERT WITH CHECK (company_id = public.current_active_company_id() AND public.current_user_can_write())';
- EXECUTE 'CREATE POLICY "salary_payments_update" ON public.salary_payments FOR UPDATE USING (company_id = public.current_active_company_id() AND public.current_user_can_write())';
- EXECUTE 'CREATE POLICY "salary_payments_delete" ON public.salary_payments FOR DELETE USING (company_id = public.current_active_company_id() AND public.current_user_can_write())';
- END IF;
-END $$;
-
-DO $$ BEGIN
- IF EXISTS (
- SELECT 1 FROM information_schema.tables
- WHERE table_schema = 'public' AND table_name = 'mileage_entries'
- ) THEN
- EXECUTE 'DROP POLICY IF EXISTS "mileage_entries_insert" ON public.mileage_entries';
- EXECUTE 'DROP POLICY IF EXISTS "mileage_entries_update" ON public.mileage_entries';
- EXECUTE 'DROP POLICY IF EXISTS "mileage_entries_delete" ON public.mileage_entries';
- EXECUTE 'CREATE POLICY "mileage_entries_insert" ON public.mileage_entries FOR INSERT WITH CHECK (company_id = public.current_active_company_id() AND public.current_user_can_write())';
- EXECUTE 'CREATE POLICY "mileage_entries_update" ON public.mileage_entries FOR UPDATE USING (company_id = public.current_active_company_id() AND public.current_user_can_write())';
- EXECUTE 'CREATE POLICY "mileage_entries_delete" ON public.mileage_entries FOR DELETE USING (company_id = public.current_active_company_id() AND public.current_user_can_write())';
- END IF;
-END $$;
diff --git a/supabase/migrations/20260412120000_default_voucher_series.sql b/supabase/migrations/20260412120000_default_voucher_series.sql
deleted file mode 100644
index 85c00454..00000000
--- a/supabase/migrations/20260412120000_default_voucher_series.sql
+++ /dev/null
@@ -1,11 +0,0 @@
--- Add default voucher series to company_settings
--- Allows companies to configure which series (A-Z) is pre-selected
--- when creating manual journal entries. Defaults to 'A'.
-
-ALTER TABLE public.company_settings
- ADD COLUMN IF NOT EXISTS default_voucher_series text NOT NULL DEFAULT 'A';
-
--- Enforce single uppercase letter
-ALTER TABLE public.company_settings
- ADD CONSTRAINT company_settings_default_voucher_series_check
- CHECK (default_voucher_series ~ '^[A-Z]$');
diff --git a/supabase/migrations/20260413100000_get_unlinked_bank_lines.sql b/supabase/migrations/20260413100000_get_unlinked_bank_lines.sql
deleted file mode 100644
index d36c6d79..00000000
--- a/supabase/migrations/20260413100000_get_unlinked_bank_lines.sql
+++ /dev/null
@@ -1,52 +0,0 @@
--- Generalized version of get_unlinked_1930_lines that accepts any bank account number.
--- This allows reconciliation against secondary bank accounts (e.g. 1931, 1932).
-
-CREATE FUNCTION public.get_unlinked_bank_lines(
- p_company_id UUID,
- p_date_from DATE DEFAULT NULL,
- p_date_to DATE DEFAULT NULL,
- p_account_number TEXT DEFAULT '1930'
-)
-RETURNS TABLE (
- line_id UUID,
- journal_entry_id UUID,
- debit_amount NUMERIC,
- credit_amount NUMERIC,
- line_description TEXT,
- entry_date DATE,
- voucher_number INT,
- voucher_series TEXT,
- entry_description TEXT,
- source_type TEXT
-)
-LANGUAGE sql
-STABLE
-SECURITY DEFINER
-SET search_path = public
-AS $$
- SELECT
- jel.id AS line_id,
- je.id AS journal_entry_id,
- jel.debit_amount,
- jel.credit_amount,
- jel.line_description,
- je.entry_date,
- je.voucher_number,
- je.voucher_series,
- je.description AS entry_description,
- je.source_type
- FROM public.journal_entry_lines jel
- JOIN public.journal_entries je ON je.id = jel.journal_entry_id
- WHERE jel.account_number = p_account_number
- AND je.company_id = p_company_id
- AND je.status = 'posted'
- AND (p_date_from IS NULL OR je.entry_date >= p_date_from)
- AND (p_date_to IS NULL OR je.entry_date <= p_date_to)
- AND NOT EXISTS (
- SELECT 1
- FROM public.transactions t
- WHERE t.journal_entry_id = je.id
- AND t.company_id = p_company_id
- )
- ORDER BY je.entry_date, je.voucher_number;
-$$;
diff --git a/supabase/migrations/20260413120000_sie_imports_replaced_status.sql b/supabase/migrations/20260413120000_sie_imports_replaced_status.sql
deleted file mode 100644
index 88bc21d4..00000000
--- a/supabase/migrations/20260413120000_sie_imports_replaced_status.sql
+++ /dev/null
@@ -1,79 +0,0 @@
--- Allow completed SIE imports to be marked as 'replaced' when a user wants to
--- re-import corrected data for the same fiscal period.
---
--- Compliance: replaced imports and their cancelled journal entries remain in the
--- database as audit trail per BFL 5 kap 5§ (rättelse) and BFNAR 2013:2 kap 8
--- (behandlingshistorik). Nothing is deleted.
-
--- 1. Expand status CHECK to include 'replaced'
-ALTER TABLE public.sie_imports
- DROP CONSTRAINT IF EXISTS sie_imports_status_check;
-ALTER TABLE public.sie_imports
- ADD CONSTRAINT sie_imports_status_check
- CHECK (status IN ('pending', 'mapped', 'completed', 'failed', 'replaced'));
-
--- 2. Add audit column for tracking when the import was replaced
-ALTER TABLE public.sie_imports
- ADD COLUMN IF NOT EXISTS replaced_at timestamptz;
-
--- 3. Convert UNIQUE (company_id, file_hash) to a partial unique index that
--- excludes replaced/failed imports. This allows re-importing the same file
--- after a previous import has been replaced.
-ALTER TABLE public.sie_imports
- DROP CONSTRAINT IF EXISTS sie_imports_company_id_file_hash_key;
-
-CREATE UNIQUE INDEX IF NOT EXISTS sie_imports_company_id_file_hash_active_idx
- ON public.sie_imports (company_id, file_hash)
- WHERE status NOT IN ('replaced', 'failed');
-
--- 4. Atomic RPC to cancel entries and mark import as replaced in one transaction.
--- Prevents inconsistent state where entries are cancelled but import stays 'completed'.
-CREATE OR REPLACE FUNCTION public.replace_sie_import(
- p_company_id uuid,
- p_import_id uuid
-) RETURNS integer
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
-AS $$
-DECLARE
- v_cancelled integer;
- v_fiscal_period_id uuid;
- v_opening_balance_entry_id uuid;
-BEGIN
- -- Look up the import record (caller must have verified status/permissions)
- SELECT fiscal_period_id, opening_balance_entry_id
- INTO v_fiscal_period_id, v_opening_balance_entry_id
- FROM public.sie_imports
- WHERE id = p_import_id AND company_id = p_company_id AND status = 'completed';
-
- IF NOT FOUND THEN
- RAISE EXCEPTION 'Import % not found or not in completed status', p_import_id;
- END IF;
-
- -- Cancel all journal entries belonging to this import
- UPDATE public.journal_entries
- SET status = 'cancelled'
- WHERE company_id = p_company_id
- AND status = 'posted'
- AND id IN (
- -- Opening balance entry
- SELECT v_opening_balance_entry_id WHERE v_opening_balance_entry_id IS NOT NULL
- UNION ALL
- -- Imported vouchers + migration adjustment
- SELECT je.id FROM public.journal_entries je
- WHERE je.company_id = p_company_id
- AND je.fiscal_period_id = v_fiscal_period_id
- AND je.source_type = 'import'
- AND je.status = 'posted'
- );
- GET DIAGNOSTICS v_cancelled = ROW_COUNT;
-
- -- Mark import as replaced
- UPDATE public.sie_imports
- SET status = 'replaced', replaced_at = now()
- WHERE id = p_import_id AND company_id = p_company_id;
-
- RETURN v_cancelled;
-END;
-$$;
diff --git a/supabase/migrations/20260413120001_pgrst_schema_reload.sql b/supabase/migrations/20260413120001_pgrst_schema_reload.sql
deleted file mode 100644
index 07760d3f..00000000
--- a/supabase/migrations/20260413120001_pgrst_schema_reload.sql
+++ /dev/null
@@ -1,4 +0,0 @@
--- Retroactive schema cache reload after recent ALTER TABLE migrations
--- (trade_name, delivery_date, pays_salaries, etc.) that did not include
--- NOTIFY pgrst. Ensures PostgREST picks up all new columns immediately.
-NOTIFY pgrst, 'reload schema';
diff --git a/supabase/migrations/20260413120002_add_journal_entry_notes.sql b/supabase/migrations/20260413120002_add_journal_entry_notes.sql
deleted file mode 100644
index d672208c..00000000
--- a/supabase/migrations/20260413120002_add_journal_entry_notes.sql
+++ /dev/null
@@ -1,73 +0,0 @@
--- Add optional notes/comment field to journal entries.
--- Notes are internal metadata (not part of BFL verifikation content)
--- and remain editable even after posting, following the Fortnox model.
-ALTER TABLE public.journal_entries ADD COLUMN notes text;
-
--- Update the immutability trigger to allow notes-only updates on posted entries.
--- BFL 5 kap 7§ defines verifikation content (date, description, amount, counterparty,
--- number, underlag refs). Notes are NOT in that list — they are internal metadata.
-CREATE OR REPLACE FUNCTION public.enforce_journal_entry_immutability()
-RETURNS trigger LANGUAGE plpgsql AS $$
-BEGIN
- -- Session variable bypass for controlled operations (e.g. delete_last_voucher RPC)
- IF current_setting('gnubok.allow_delete', true) = 'true' THEN
- IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
- RETURN NEW;
- END IF;
-
- IF TG_OP = 'DELETE' THEN
- -- No exemption for drafts: varaktighet applies from insertion.
- -- Application code uses status='cancelled' instead of DELETE.
- RAISE EXCEPTION 'Cannot delete journal entries (id: %, status: %). Use cancelled status instead.',
- OLD.id, OLD.status;
- END IF;
-
- -- Draft can transition to draft (update fields), posted, or cancelled
- IF OLD.status = 'draft' AND NEW.status IN ('draft', 'posted', 'cancelled') THEN
- RETURN NEW;
- END IF;
-
- -- Posted entries: allow notes-only updates (internal metadata, not verifikation content)
- -- BFL 5 kap 7§ mandates immutability for verifikation fields (description, date, amounts,
- -- voucher number, etc.). Notes/comments are outside this scope per Fortnox precedent.
- IF OLD.status = 'posted' AND NEW.status = 'posted' THEN
- IF NEW.description = OLD.description
- AND NEW.entry_date = OLD.entry_date
- AND NEW.fiscal_period_id = OLD.fiscal_period_id
- AND NEW.voucher_number = OLD.voucher_number
- AND NEW.voucher_series = OLD.voucher_series
- AND NEW.source_type = OLD.source_type
- AND COALESCE(NEW.source_id::text, '') = COALESCE(OLD.source_id::text, '')
- AND NEW.user_id = OLD.user_id
- AND COALESCE(NEW.reversed_by_id::text, '') = COALESCE(OLD.reversed_by_id::text, '')
- AND COALESCE(NEW.reverses_id::text, '') = COALESCE(OLD.reverses_id::text, '')
- AND COALESCE(NEW.correction_of_id::text, '') = COALESCE(OLD.correction_of_id::text, '')
- AND NEW.committed_at IS NOT DISTINCT FROM OLD.committed_at
- THEN
- RETURN NEW; -- Only notes and updated_at may differ
- END IF;
- END IF;
-
- -- Posted can transition to reversed (storno) or cancelled (orphaned cleanup)
- IF OLD.status = 'posted' AND NEW.status IN ('reversed', 'cancelled') THEN
- IF NEW.status = 'reversed' THEN
- IF NEW.description != OLD.description OR NEW.entry_date != OLD.entry_date
- OR NEW.fiscal_period_id != OLD.fiscal_period_id
- OR NEW.voucher_number != OLD.voucher_number THEN
- RAISE EXCEPTION 'Cannot modify fields of a posted entry during reversal (id: %)', OLD.id;
- END IF;
- END IF;
- RETURN NEW;
- END IF;
-
- -- Reversed entries can transition back to posted (when their storno is deleted)
- IF OLD.status = 'reversed' AND NEW.status = 'posted'
- AND current_setting('gnubok.allow_delete', true) = 'true' THEN
- RETURN NEW;
- END IF;
-
- RAISE EXCEPTION 'Cannot modify a % journal entry (id: %). Committed entries are immutable per Bokforingslagen.',
- OLD.status, OLD.id;
-END; $$;
-
-NOTIFY pgrst, 'reload schema';
diff --git a/supabase/migrations/20260413120003_delete_last_voucher.sql b/supabase/migrations/20260413120003_delete_last_voucher.sql
deleted file mode 100644
index 7fec893d..00000000
--- a/supabase/migrations/20260413120003_delete_last_voucher.sql
+++ /dev/null
@@ -1,223 +0,0 @@
--- Delete last voucher per series (Fortnox model).
--- Allows deleting the highest-numbered posted voucher in a series,
--- maintaining an unbroken voucher number sequence per BFL 5 kap 7§.
---
--- Safeguards:
--- 1. Only the LAST voucher in its (company, fiscal_period, series) can be deleted
--- 2. Fiscal period must not be closed or locked
--- 3. No other entries may reference it (reverses_id, correction_of_id)
--- 4. Calling user must be company owner or admin
--- 5. Full audit trail via write_audit_log() trigger (BFNAR 2013:2 behandlingshistorik)
-
--- 1. Update line immutability trigger to respect session variable bypass
-CREATE OR REPLACE FUNCTION public.enforce_journal_entry_line_immutability()
-RETURNS trigger LANGUAGE plpgsql AS $$
-DECLARE v_status text;
-BEGIN
- -- Session variable bypass for controlled operations (delete_last_voucher RPC)
- IF current_setting('gnubok.allow_delete', true) = 'true' THEN
- IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
- RETURN NEW;
- END IF;
-
- SELECT status INTO v_status FROM public.journal_entries
- WHERE id = COALESCE(OLD.journal_entry_id, NEW.journal_entry_id);
-
- -- Draft entries: all operations allowed
- IF v_status = 'draft' THEN
- IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
- RETURN NEW;
- END IF;
-
- -- Cancelled entries: only DELETE for cleanup
- IF v_status = 'cancelled' THEN
- IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
- RAISE EXCEPTION 'Cannot % lines of a cancelled journal entry.', TG_OP;
- END IF;
-
- RAISE EXCEPTION 'Cannot % lines of a % journal entry.', TG_OP, v_status;
-END; $$;
-
--- 2. Update retention trigger to respect session variable bypass
-CREATE OR REPLACE FUNCTION public.enforce_retention_journal_entries()
-RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER AS $$
-DECLARE
- v_retention_expires date;
-BEGIN
- -- Session variable bypass for controlled operations (delete_last_voucher RPC)
- IF current_setting('gnubok.allow_delete', true) = 'true' THEN
- RETURN OLD;
- END IF;
-
- SELECT fp.retention_expires_at INTO v_retention_expires
- FROM public.fiscal_periods fp
- WHERE fp.id = OLD.fiscal_period_id;
-
- IF v_retention_expires IS NOT NULL AND v_retention_expires > CURRENT_DATE THEN
- INSERT INTO public.audit_log (user_id, action, table_name, record_id, description)
- VALUES (OLD.user_id, 'RETENTION_BLOCK', 'journal_entries', OLD.id,
- 'Attempted deletion within retention period (expires ' || v_retention_expires || ')');
-
- RAISE EXCEPTION 'Cannot delete journal entry within 7-year retention period (expires %)',
- v_retention_expires;
- END IF;
-
- RETURN OLD;
-END; $$;
-
--- 3. Create the delete_last_voucher RPC
-CREATE OR REPLACE FUNCTION public.delete_last_voucher(
- p_company_id uuid,
- p_entry_id uuid
-)
-RETURNS jsonb
-LANGUAGE plpgsql
-SECURITY DEFINER
-SET search_path = public
-AS $$
-DECLARE
- v_entry record;
- v_period record;
- v_max_voucher integer;
- v_ref_count integer;
- v_caller_role text;
- v_snapshot jsonb;
- v_lines_snapshot jsonb;
-BEGIN
- -- 1. Verify calling user is company owner or admin
- SELECT cm.role INTO v_caller_role
- FROM company_members cm
- WHERE cm.company_id = p_company_id
- AND cm.user_id = auth.uid();
-
- IF v_caller_role IS NULL OR v_caller_role NOT IN ('owner', 'admin') THEN
- RAISE EXCEPTION 'Only company owners and admins can delete vouchers';
- END IF;
-
- -- 2. Lock and fetch the target entry
- SELECT * INTO v_entry
- FROM journal_entries
- WHERE id = p_entry_id
- AND company_id = p_company_id
- FOR UPDATE;
-
- IF v_entry IS NULL THEN
- RAISE EXCEPTION 'Journal entry not found';
- END IF;
-
- IF v_entry.status != 'posted' THEN
- RAISE EXCEPTION 'Only posted entries can be deleted (current status: %)', v_entry.status;
- END IF;
-
- -- 3. Verify fiscal period is open
- SELECT * INTO v_period
- FROM fiscal_periods
- WHERE id = v_entry.fiscal_period_id
- FOR UPDATE;
-
- IF v_period.is_closed THEN
- RAISE EXCEPTION 'Cannot delete voucher in a closed fiscal period';
- END IF;
-
- IF v_period.locked_at IS NOT NULL THEN
- RAISE EXCEPTION 'Cannot delete voucher in a locked fiscal period';
- END IF;
-
- -- 4. Lock voucher_sequences row to serialise against concurrent commit_journal_entry.
- -- Without this, a concurrent commit could assign a new voucher number between
- -- our MAX check and the DELETE, producing a gap (violating BFL 5 kap 7§).
- PERFORM 1 FROM voucher_sequences
- WHERE company_id = p_company_id
- AND fiscal_period_id = v_entry.fiscal_period_id
- AND voucher_series = v_entry.voucher_series
- FOR UPDATE;
-
- -- Verify it is the LAST voucher in its series
- SELECT MAX(voucher_number) INTO v_max_voucher
- FROM journal_entries
- WHERE company_id = p_company_id
- AND fiscal_period_id = v_entry.fiscal_period_id
- AND voucher_series = v_entry.voucher_series
- AND status NOT IN ('cancelled', 'draft');
-
- IF v_entry.voucher_number != v_max_voucher THEN
- RAISE EXCEPTION 'Kan bara radera det sista verifikatet i serien. % har nummer % men senaste är %',
- v_entry.voucher_series, v_entry.voucher_number, v_max_voucher;
- END IF;
-
- -- 5. Verify no other entries reference this one
- SELECT COUNT(*) INTO v_ref_count
- FROM journal_entries
- WHERE company_id = p_company_id
- AND status != 'cancelled'
- AND (reverses_id = p_entry_id OR correction_of_id = p_entry_id);
-
- IF v_ref_count > 0 THEN
- RAISE EXCEPTION 'Cannot delete: other entries reference this voucher (% references)',
- v_ref_count;
- END IF;
-
- -- 6. Capture snapshot for audit trail (BFNAR 2013:2 behandlingshistorik)
- -- The write_audit_log() AFTER trigger also captures old_state, but we
- -- include lines here for complete traceability.
- SELECT jsonb_agg(to_jsonb(l)) INTO v_lines_snapshot
- FROM journal_entry_lines l
- WHERE l.journal_entry_id = p_entry_id;
-
- v_snapshot := to_jsonb(v_entry) || jsonb_build_object('lines', COALESCE(v_lines_snapshot, '[]'::jsonb));
-
- -- 7. If this entry is a storno (reverses another entry), restore the original
- IF v_entry.reverses_id IS NOT NULL THEN
- -- Use session variable to allow reversed → posted transition
- PERFORM set_config('gnubok.allow_delete', 'true', true);
- UPDATE journal_entries
- SET status = 'posted', reversed_by_id = NULL
- WHERE id = v_entry.reverses_id
- AND company_id = p_company_id;
- END IF;
-
- -- 8. Enable session variable bypass for deletion triggers
- PERFORM set_config('gnubok.allow_delete', 'true', true);
-
- -- 9. Unlink document attachments (FK is RESTRICT, must clear before delete)
- UPDATE document_attachments
- SET journal_entry_id = NULL
- WHERE journal_entry_id = p_entry_id;
-
- -- 10. Delete the journal entry
- -- journal_entry_lines: ON DELETE CASCADE (auto-deleted)
- -- transactions: ON DELETE SET NULL (auto-nullified)
- -- supplier_invoices: ON DELETE SET NULL (auto-nullified)
- -- supplier_invoice_payments: ON DELETE SET NULL (auto-nullified)
- -- invoice_payments: ON DELETE SET NULL (auto-nullified)
- DELETE FROM journal_entries WHERE id = p_entry_id;
-
- -- 11. Decrement voucher sequence
- UPDATE voucher_sequences
- SET last_number = GREATEST(last_number - 1, 0)
- WHERE company_id = p_company_id
- AND fiscal_period_id = v_entry.fiscal_period_id
- AND voucher_series = v_entry.voucher_series;
-
- -- 12. Insert explicit audit entry with full snapshot including lines
- INSERT INTO audit_log (user_id, action, table_name, record_id, actor_id, old_state, description)
- VALUES (
- v_entry.user_id,
- 'DELETE',
- 'journal_entries',
- p_entry_id,
- auth.uid(),
- v_snapshot,
- 'Deleted voucher ' || v_entry.voucher_series || v_entry.voucher_number ||
- ' (delete_last_voucher RPC, caller: ' || auth.uid() || ')'
- );
-
- RETURN jsonb_build_object(
- 'deleted', true,
- 'voucher_series', v_entry.voucher_series,
- 'voucher_number', v_entry.voucher_number
- );
-END;
-$$;
-
-NOTIFY pgrst, 'reload schema';
diff --git a/supabase/migrations/20260413140000_check_email_exists_rpc.sql b/supabase/migrations/20260413140000_check_email_exists_rpc.sql
deleted file mode 100644
index 2dce642f..00000000
--- a/supabase/migrations/20260413140000_check_email_exists_rpc.sql
+++ /dev/null
@@ -1,19 +0,0 @@
--- Efficient email existence check for invite flow.
--- Replaces the previous approach of listing all auth users (which only
--- returned the first page and broke for instances with >50 users).
-CREATE OR REPLACE FUNCTION public.check_email_exists(email_to_check text)
-RETURNS boolean
-LANGUAGE sql
-SECURITY DEFINER
-SET search_path = ''
-AS $$
- SELECT EXISTS (
- SELECT 1 FROM auth.users WHERE lower(email) = lower(email_to_check)
- );
-$$;
-
--- Only callable by service role — prevents email enumeration via PostgREST.
--- Must revoke from PUBLIC first (PostgreSQL grants EXECUTE to PUBLIC by default),
--- then grant explicitly to service_role.
-REVOKE EXECUTE ON FUNCTION public.check_email_exists(text) FROM PUBLIC;
-GRANT EXECUTE ON FUNCTION public.check_email_exists(text) TO service_role;
diff --git a/supabase/migrations/20260413150000_viewer_bank_import_permissions.sql b/supabase/migrations/20260413150000_viewer_bank_import_permissions.sql
deleted file mode 100644
index 2fc699e6..00000000
--- a/supabase/migrations/20260413150000_viewer_bank_import_permissions.sql
+++ /dev/null
@@ -1,78 +0,0 @@
--- =============================================================================
--- Viewer role: allow bank transaction import and bank connection
--- =============================================================================
---
--- Viewers are read-only by default (enforced by `current_user_can_write()`).
--- This migration adds additive policies that let viewers:
--- 1. Import bank files (INSERT transactions + bank_file_imports)
--- 2. Connect banks via PSD2 (INSERT + UPDATE bank_connections)
---
--- No other write operation is opened — viewers still cannot categorize,
--- book, edit, or delete transactions, create invoices, etc.
---
--- RLS is OR-based: existing policies (which require `current_user_can_write()`)
--- remain unchanged. These new policies provide an alternative path for
--- viewers on just these tables.
--- =============================================================================
-
--- ─── Transactions ────────────────────────────────────────────────────────────
--- Viewer can INSERT (not UPDATE/DELETE) — raw uncategorized transactions only
-CREATE POLICY "transactions_viewer_insert" ON public.transactions
- FOR INSERT WITH CHECK (
- company_id = public.current_active_company_id()
- AND EXISTS (
- SELECT 1 FROM public.company_members cm
- WHERE cm.user_id = auth.uid()
- AND cm.company_id = public.current_active_company_id()
- AND cm.role = 'viewer'
- )
- );
-
--- ─── Bank file imports ───────────────────────────────────────────────────────
--- Viewer can INSERT (create import record) + UPDATE (status tracking)
-CREATE POLICY "bank_file_imports_viewer_insert" ON public.bank_file_imports
- FOR INSERT WITH CHECK (
- company_id = public.current_active_company_id()
- AND EXISTS (
- SELECT 1 FROM public.company_members cm
- WHERE cm.user_id = auth.uid()
- AND cm.company_id = public.current_active_company_id()
- AND cm.role = 'viewer'
- )
- );
-
-CREATE POLICY "bank_file_imports_viewer_update" ON public.bank_file_imports
- FOR UPDATE USING (
- company_id = public.current_active_company_id()
- AND EXISTS (
- SELECT 1 FROM public.company_members cm
- WHERE cm.user_id = auth.uid()
- AND cm.company_id = public.current_active_company_id()
- AND cm.role = 'viewer'
- )
- );
-
--- ─── Bank connections ────────────────────────────────────────────────────────
--- Viewer can INSERT (initiate PSD2 connection) + UPDATE (status changes, sync)
--- No DELETE — disconnecting sets status='revoked' via UPDATE, not DELETE
-CREATE POLICY "bank_connections_viewer_insert" ON public.bank_connections
- FOR INSERT WITH CHECK (
- company_id = public.current_active_company_id()
- AND EXISTS (
- SELECT 1 FROM public.company_members cm
- WHERE cm.user_id = auth.uid()
- AND cm.company_id = public.current_active_company_id()
- AND cm.role = 'viewer'
- )
- );
-
-CREATE POLICY "bank_connections_viewer_update" ON public.bank_connections
- FOR UPDATE USING (
- company_id = public.current_active_company_id()
- AND EXISTS (
- SELECT 1 FROM public.company_members cm
- WHERE cm.user_id = auth.uid()
- AND cm.company_id = public.current_active_company_id()
- AND cm.role = 'viewer'
- )
- );
diff --git a/supabase/migrations/20260415000000_schema_sync.sql b/supabase/migrations/20260415000000_schema_sync.sql
new file mode 100644
index 00000000..fa7d87f5
--- /dev/null
+++ b/supabase/migrations/20260415000000_schema_sync.sql
@@ -0,0 +1,979 @@
+-- Schema sync: consolidates all changes applied to production after migration 97
+-- (20260402120000_inbox_classification). This single migration reproduces the exact
+-- production schema delta when applied on top of the first 97 base migrations.
+--
+-- Changes consolidated from 20 remote migrations (20260407091315 – 20260414191533).
+-- Production database is the source of truth — no schema changes are introduced.
+
+
+-- ============================================================================
+-- 1. NEW TABLES
+-- ============================================================================
+
+-- 1a. bankid_identities
+CREATE TABLE IF NOT EXISTS public.bankid_identities (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+ user_id uuid REFERENCES auth.users ON DELETE CASCADE UNIQUE NOT NULL,
+ personal_number_hash text NOT NULL,
+ personal_number_enc bytea NOT NULL,
+ given_name text,
+ surname text,
+ linked_at timestamptz NOT NULL DEFAULT now(),
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz NOT NULL DEFAULT now()
+);
+ALTER TABLE public.bankid_identities ENABLE ROW LEVEL SECURITY;
+
+CREATE UNIQUE INDEX IF NOT EXISTS idx_bankid_identities_pnr_hash
+ ON public.bankid_identities (personal_number_hash);
+CREATE INDEX IF NOT EXISTS idx_bankid_identities_user_id
+ ON public.bankid_identities (user_id);
+
+DO $$ BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='bankid_identities' AND policyname='bankid_identities_select') THEN
+ CREATE POLICY bankid_identities_select ON public.bankid_identities FOR SELECT USING (auth.uid() = user_id);
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='bankid_identities' AND policyname='bankid_identities_insert') THEN
+ CREATE POLICY bankid_identities_insert ON public.bankid_identities FOR INSERT WITH CHECK (auth.uid() = user_id);
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='bankid_identities' AND policyname='bankid_identities_delete') THEN
+ CREATE POLICY bankid_identities_delete ON public.bankid_identities FOR DELETE USING (auth.uid() = user_id);
+ END IF;
+END $$;
+
+CREATE OR REPLACE TRIGGER bankid_identities_updated_at
+ BEFORE UPDATE ON public.bankid_identities
+ FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
+
+
+-- 1b. automation_webhooks
+CREATE TABLE IF NOT EXISTS public.automation_webhooks (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+ company_id uuid NOT NULL REFERENCES public.companies ON DELETE CASCADE,
+ event_type text NOT NULL,
+ webhook_url text NOT NULL,
+ active boolean NOT NULL DEFAULT true,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz NOT NULL DEFAULT now(),
+ UNIQUE (company_id, event_type)
+);
+ALTER TABLE public.automation_webhooks ENABLE ROW LEVEL SECURITY;
+
+CREATE INDEX IF NOT EXISTS idx_automation_webhooks_company_event
+ ON public.automation_webhooks (company_id, event_type) WHERE (active = true);
+
+DO $$ BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='automation_webhooks' AND policyname='Members can view company webhooks') THEN
+ CREATE POLICY "Members can view company webhooks" ON public.automation_webhooks FOR SELECT
+ USING (company_id IN (SELECT company_id FROM company_members WHERE user_id = auth.uid()));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='automation_webhooks' AND policyname='Members can insert company webhooks') THEN
+ CREATE POLICY "Members can insert company webhooks" ON public.automation_webhooks FOR INSERT
+ WITH CHECK (company_id IN (SELECT company_id FROM company_members WHERE user_id = auth.uid()));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='automation_webhooks' AND policyname='Members can update company webhooks') THEN
+ CREATE POLICY "Members can update company webhooks" ON public.automation_webhooks FOR UPDATE
+ USING (company_id IN (SELECT company_id FROM company_members WHERE user_id = auth.uid()));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='automation_webhooks' AND policyname='Members can delete company webhooks') THEN
+ CREATE POLICY "Members can delete company webhooks" ON public.automation_webhooks FOR DELETE
+ USING (company_id IN (SELECT company_id FROM company_members WHERE user_id = auth.uid()));
+ END IF;
+END $$;
+
+CREATE OR REPLACE TRIGGER set_updated_at
+ BEFORE UPDATE ON public.automation_webhooks
+ FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
+
+
+-- 1c. booking_template_library
+CREATE TABLE IF NOT EXISTS public.booking_template_library (
+ id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
+ company_id uuid REFERENCES public.companies ON DELETE CASCADE,
+ team_id uuid REFERENCES public.teams ON DELETE CASCADE,
+ created_by uuid,
+ name text NOT NULL,
+ description text NOT NULL DEFAULT '',
+ category text NOT NULL DEFAULT 'other'
+ CHECK (category IN ('eu_trade','tax_account','private_transfer','salary','representation','year_end','vat','financial','other')),
+ entity_type text NOT NULL DEFAULT 'all'
+ CHECK (entity_type IN ('all','enskild_firma','aktiebolag')),
+ lines jsonb NOT NULL DEFAULT '[]'::jsonb,
+ is_system boolean NOT NULL DEFAULT false,
+ is_active boolean NOT NULL DEFAULT true,
+ created_at timestamptz NOT NULL DEFAULT now(),
+ updated_at timestamptz NOT NULL DEFAULT now(),
+ CHECK (NOT is_system OR (company_id IS NULL AND team_id IS NULL)),
+ CHECK (team_id IS NULL OR company_id IS NULL),
+ CHECK (company_id IS NOT NULL OR team_id IS NOT NULL OR is_system)
+);
+ALTER TABLE public.booking_template_library ENABLE ROW LEVEL SECURITY;
+
+CREATE INDEX IF NOT EXISTS idx_btl_active ON public.booking_template_library (is_active) WHERE (is_active = true);
+CREATE INDEX IF NOT EXISTS idx_btl_category ON public.booking_template_library (category);
+CREATE INDEX IF NOT EXISTS idx_btl_company ON public.booking_template_library (company_id) WHERE (company_id IS NOT NULL);
+CREATE INDEX IF NOT EXISTS idx_btl_system ON public.booking_template_library (is_system) WHERE (is_system = true);
+CREATE INDEX IF NOT EXISTS idx_btl_team ON public.booking_template_library (team_id) WHERE (team_id IS NOT NULL);
+
+DO $$ BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='booking_template_library' AND policyname='btl_select') THEN
+ CREATE POLICY btl_select ON public.booking_template_library FOR SELECT
+ USING (is_system OR company_id IN (SELECT user_company_ids()) OR team_id IN (SELECT user_team_ids()));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='booking_template_library' AND policyname='btl_insert') THEN
+ CREATE POLICY btl_insert ON public.booking_template_library FOR INSERT
+ WITH CHECK (NOT is_system AND (company_id IN (SELECT user_company_ids()) OR (company_id IS NULL AND team_id IN (SELECT user_team_ids()))));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='booking_template_library' AND policyname='btl_update') THEN
+ CREATE POLICY btl_update ON public.booking_template_library FOR UPDATE
+ USING (NOT is_system AND (company_id IN (SELECT user_company_ids()) OR (company_id IS NULL AND team_id IN (SELECT user_team_ids()))));
+ END IF;
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='booking_template_library' AND policyname='btl_delete') THEN
+ CREATE POLICY btl_delete ON public.booking_template_library FOR DELETE
+ USING (NOT is_system AND (company_id IN (SELECT user_company_ids()) OR (company_id IS NULL AND team_id IN (SELECT user_team_ids()))));
+ END IF;
+END $$;
+
+CREATE OR REPLACE TRIGGER btl_updated_at
+ BEFORE UPDATE ON public.booking_template_library
+ FOR EACH ROW EXECUTE FUNCTION public.update_updated_at_column();
+
+
+-- ============================================================================
+-- 2. ALTER EXISTING TABLES — new columns
+-- ============================================================================
+
+ALTER TABLE public.companies ADD COLUMN IF NOT EXISTS archived_by uuid;
+ALTER TABLE public.invoices ADD COLUMN IF NOT EXISTS delivery_date date;
+ALTER TABLE public.journal_entries ADD COLUMN IF NOT EXISTS notes text;
+ALTER TABLE public.sie_imports ADD COLUMN IF NOT EXISTS replaced_at timestamptz;
+ALTER TABLE public.document_attachments ADD COLUMN IF NOT EXISTS prev_version_hash text;
+
+
+-- ============================================================================
+-- 3. MODIFIED CHECK CONSTRAINTS
+-- ============================================================================
+
+-- sie_imports: add 'replaced' status
+ALTER TABLE public.sie_imports DROP CONSTRAINT IF EXISTS sie_imports_status_check;
+ALTER TABLE public.sie_imports ADD CONSTRAINT sie_imports_status_check
+ CHECK (status = ANY (ARRAY['pending','mapped','completed','failed','replaced']));
+
+-- audit_log: add integrity/security action types
+ALTER TABLE public.audit_log DROP CONSTRAINT IF EXISTS audit_log_action_check;
+ALTER TABLE public.audit_log ADD CONSTRAINT audit_log_action_check
+ CHECK (action = ANY (ARRAY[
+ 'INSERT','UPDATE','DELETE','COMMIT','REVERSE','CORRECT',
+ 'LOCK_PERIOD','CLOSE_PERIOD','DOCUMENT_DELETE_BLOCKED',
+ 'RETENTION_BLOCK','SECURITY_EVENT','INTEGRITY_FAILURE'
+ ]));
+
+
+-- ============================================================================
+-- 4. FUNCTIONS — CREATE OR REPLACE
+-- ============================================================================
+
+-- 4a. user_company_ids — filters out archived companies
+CREATE OR REPLACE FUNCTION public.user_company_ids()
+ RETURNS SETOF uuid
+ LANGUAGE sql
+ STABLE SECURITY DEFINER
+ SET search_path TO 'public'
+AS $function$
+ SELECT cm.company_id
+ FROM public.company_members cm
+ JOIN public.companies c ON c.id = cm.company_id
+ WHERE cm.user_id = auth.uid()
+ AND c.archived_at IS NULL;
+$function$;
+
+-- 4b. ensure_user_team — get-or-create personal team
+CREATE OR REPLACE FUNCTION public.ensure_user_team()
+ RETURNS uuid
+ LANGUAGE plpgsql
+ SECURITY DEFINER
+ SET search_path TO 'public'
+AS $function$
+DECLARE
+ v_user_id uuid;
+ v_team_id uuid;
+BEGIN
+ v_user_id := auth.uid();
+ IF v_user_id IS NULL THEN
+ RAISE EXCEPTION 'Not authenticated';
+ END IF;
+
+ SELECT team_id INTO v_team_id
+ FROM public.team_members
+ WHERE user_id = v_user_id
+ LIMIT 1;
+
+ IF v_team_id IS NOT NULL THEN
+ RETURN v_team_id;
+ END IF;
+
+ INSERT INTO public.teams (name, created_by)
+ VALUES ('Personal', v_user_id)
+ RETURNING id INTO v_team_id;
+
+ INSERT INTO public.team_members (team_id, user_id, role)
+ VALUES (v_team_id, v_user_id, 'owner');
+
+ RETURN v_team_id;
+END;
+$function$;
+
+-- 4c. enforce_journal_entry_immutability — supports gnubok.allow_delete + cancelled
+CREATE OR REPLACE FUNCTION public.enforce_journal_entry_immutability()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$
+BEGIN
+ IF current_setting('gnubok.allow_delete', true) = 'true' THEN
+ IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
+ RETURN NEW;
+ END IF;
+
+ IF TG_OP = 'DELETE' THEN
+ RAISE EXCEPTION 'Cannot delete journal entries (id: %, status: %). Use cancelled status instead.',
+ OLD.id, OLD.status;
+ END IF;
+
+ IF OLD.status = 'draft' AND NEW.status IN ('draft', 'posted', 'cancelled') THEN
+ RETURN NEW;
+ END IF;
+
+ IF OLD.status = 'posted' AND NEW.status = 'posted' THEN
+ IF NEW.description = OLD.description
+ AND NEW.entry_date = OLD.entry_date
+ AND NEW.fiscal_period_id = OLD.fiscal_period_id
+ AND NEW.voucher_number = OLD.voucher_number
+ AND NEW.voucher_series = OLD.voucher_series
+ AND NEW.source_type = OLD.source_type
+ AND COALESCE(NEW.source_id::text, '') = COALESCE(OLD.source_id::text, '')
+ AND NEW.user_id = OLD.user_id
+ AND COALESCE(NEW.reversed_by_id::text, '') = COALESCE(OLD.reversed_by_id::text, '')
+ AND COALESCE(NEW.reverses_id::text, '') = COALESCE(OLD.reverses_id::text, '')
+ AND COALESCE(NEW.correction_of_id::text, '') = COALESCE(OLD.correction_of_id::text, '')
+ AND NEW.committed_at IS NOT DISTINCT FROM OLD.committed_at
+ THEN
+ RETURN NEW;
+ END IF;
+ END IF;
+
+ IF OLD.status = 'posted' AND NEW.status IN ('reversed', 'cancelled') THEN
+ IF NEW.status = 'reversed' THEN
+ IF NEW.description != OLD.description OR NEW.entry_date != OLD.entry_date
+ OR NEW.fiscal_period_id != OLD.fiscal_period_id
+ OR NEW.voucher_number != OLD.voucher_number THEN
+ RAISE EXCEPTION 'Cannot modify fields of a posted entry during reversal (id: %)', OLD.id;
+ END IF;
+ END IF;
+ RETURN NEW;
+ END IF;
+
+ IF OLD.status = 'reversed' AND NEW.status = 'posted'
+ AND current_setting('gnubok.allow_delete', true) = 'true' THEN
+ RETURN NEW;
+ END IF;
+
+ RAISE EXCEPTION 'Cannot modify a % journal entry (id: %). Committed entries are immutable per Bokforingslagen.',
+ OLD.status, OLD.id;
+END; $function$;
+
+-- 4d. enforce_journal_entry_line_immutability — supports gnubok.allow_delete + cancelled
+CREATE OR REPLACE FUNCTION public.enforce_journal_entry_line_immutability()
+ RETURNS trigger
+ LANGUAGE plpgsql
+AS $function$
+DECLARE v_status text;
+BEGIN
+ IF current_setting('gnubok.allow_delete', true) = 'true' THEN
+ IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
+ RETURN NEW;
+ END IF;
+
+ SELECT status INTO v_status FROM public.journal_entries
+ WHERE id = COALESCE(OLD.journal_entry_id, NEW.journal_entry_id);
+
+ IF v_status = 'draft' THEN
+ IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
+ RETURN NEW;
+ END IF;
+
+ IF v_status = 'cancelled' THEN
+ IF TG_OP = 'DELETE' THEN RETURN OLD; END IF;
+ RAISE EXCEPTION 'Cannot % lines of a cancelled journal entry.', TG_OP;
+ END IF;
+
+ RAISE EXCEPTION 'Cannot % lines of a % journal entry.', TG_OP, v_status;
+END; $function$;
+
+-- 4e. enforce_retention_journal_entries — supports gnubok.allow_delete
+CREATE OR REPLACE FUNCTION public.enforce_retention_journal_entries()
+ RETURNS trigger
+ LANGUAGE plpgsql
+ SECURITY DEFINER
+AS $function$
+DECLARE
+ v_retention_expires date;
+BEGIN
+ IF current_setting('gnubok.allow_delete', true) = 'true' THEN
+ RETURN OLD;
+ END IF;
+
+ SELECT fp.retention_expires_at INTO v_retention_expires
+ FROM public.fiscal_periods fp
+ WHERE fp.id = OLD.fiscal_period_id;
+
+ IF v_retention_expires IS NOT NULL AND v_retention_expires > CURRENT_DATE THEN
+ INSERT INTO public.audit_log (user_id, action, table_name, record_id, description)
+ VALUES (OLD.user_id, 'RETENTION_BLOCK', 'journal_entries', OLD.id,
+ 'Attempted deletion within retention period (expires ' || v_retention_expires || ')');
+
+ RAISE EXCEPTION 'Cannot delete journal entry within 7-year retention period (expires %)',
+ v_retention_expires;
+ END IF;
+
+ RETURN OLD;
+END; $function$;
+
+-- 4f. enforce_document_metadata_immutability — new trigger function
+CREATE OR REPLACE FUNCTION public.enforce_document_metadata_immutability()
+ RETURNS trigger
+ LANGUAGE plpgsql
+ SECURITY DEFINER
+ SET search_path TO 'public'
+AS $function$
+DECLARE
+ v_entry_status text;
+BEGIN
+ IF OLD.journal_entry_id IS NULL THEN
+ RETURN NEW;
+ END IF;
+
+ SELECT status INTO v_entry_status
+ FROM public.journal_entries
+ WHERE id = OLD.journal_entry_id;
+
+ IF v_entry_status IS NULL OR v_entry_status NOT IN ('posted', 'reversed') THEN
+ RETURN NEW;
+ END IF;
+
+ IF NEW.file_name IS DISTINCT FROM OLD.file_name
+ OR NEW.storage_path IS DISTINCT FROM OLD.storage_path
+ OR NEW.file_size_bytes IS DISTINCT FROM OLD.file_size_bytes
+ OR NEW.mime_type IS DISTINCT FROM OLD.mime_type
+ OR NEW.sha256_hash IS DISTINCT FROM OLD.sha256_hash
+ OR NEW.upload_source IS DISTINCT FROM OLD.upload_source
+ OR NEW.digitization_date IS DISTINCT FROM OLD.digitization_date
+ OR NEW.uploaded_by IS DISTINCT FROM OLD.uploaded_by
+ OR NEW.version IS DISTINCT FROM OLD.version
+ OR NEW.original_id IS DISTINCT FROM OLD.original_id
+ OR NEW.is_current_version IS DISTINCT FROM OLD.is_current_version
+ THEN
+ INSERT INTO public.audit_log (user_id, company_id, action, table_name, record_id, description)
+ VALUES (OLD.user_id, OLD.company_id, 'SECURITY_EVENT', 'document_attachments', OLD.id,
+ 'Blocked metadata modification of document linked to ' || v_entry_status || ' entry ' || OLD.journal_entry_id);
+
+ RAISE EXCEPTION 'Cannot modify metadata of document linked to a % journal entry (BFL 7 kap)', v_entry_status;
+ END IF;
+
+ RETURN NEW;
+END;
+$function$;
+
+-- 4g. create_document_version — document versioning with hash chain
+CREATE OR REPLACE FUNCTION public.create_document_version(
+ p_user_id uuid,
+ p_original_doc_id uuid,
+ p_storage_path text,
+ p_file_name text,
+ p_file_size_bytes bigint,
+ p_mime_type text,
+ p_sha256_hash text
+)
+ RETURNS uuid
+ LANGUAGE plpgsql
+ SECURITY DEFINER
+ SET search_path TO 'public'
+AS $function$
+DECLARE
+ v_current document_attachments%ROWTYPE;
+ v_new_id uuid;
+ v_root_id uuid;
+ v_next_version integer;
+BEGIN
+ SELECT * INTO v_current
+ FROM public.document_attachments
+ WHERE id = p_original_doc_id
+ AND is_current_version = true
+ FOR UPDATE;
+
+ IF v_current IS NULL THEN
+ RAISE EXCEPTION 'Document % not found or is not the current version', p_original_doc_id;
+ END IF;
+
+ v_root_id := COALESCE(v_current.original_id, v_current.id);
+ v_next_version := v_current.version + 1;
+
+ INSERT INTO public.document_attachments (
+ user_id, company_id, storage_path, file_name, file_size_bytes,
+ mime_type, sha256_hash, version, original_id, is_current_version,
+ uploaded_by, upload_source, digitization_date,
+ journal_entry_id, journal_entry_line_id, prev_version_hash
+ ) VALUES (
+ p_user_id, v_current.company_id, p_storage_path, p_file_name,
+ p_file_size_bytes, p_mime_type, p_sha256_hash, v_next_version,
+ v_root_id, true, p_user_id, v_current.upload_source, now(),
+ v_current.journal_entry_id, v_current.journal_entry_line_id,
+ v_current.sha256_hash
+ )
+ RETURNING id INTO v_new_id;
+
+ UPDATE public.document_attachments
+ SET is_current_version = false,
+ superseded_by_id = v_new_id
+ WHERE id = p_original_doc_id;
+
+ RETURN v_new_id;
+END;
+$function$;
+
+-- 4h. validate_version_chain — validates document version hash chain
+CREATE OR REPLACE FUNCTION public.validate_version_chain(p_document_id uuid)
+ RETURNS TABLE(version integer, document_id uuid, hash_valid boolean)
+ LANGUAGE plpgsql
+ SECURITY DEFINER
+ SET search_path TO 'public'
+AS $function$
+DECLARE
+ v_root_id uuid;
+BEGIN
+ SELECT COALESCE(da.original_id, da.id) INTO v_root_id
+ FROM public.document_attachments da
+ WHERE da.id = p_document_id;
+
+ IF v_root_id IS NULL THEN
+ RAISE EXCEPTION 'Document % not found', p_document_id;
+ END IF;
+
+ RETURN QUERY
+ WITH chain AS (
+ SELECT
+ da.id AS doc_id,
+ da.version AS ver,
+ da.sha256_hash,
+ da.prev_version_hash,
+ LAG(da.sha256_hash) OVER (ORDER BY da.version) AS expected_prev_hash
+ FROM public.document_attachments da
+ WHERE da.id = v_root_id OR da.original_id = v_root_id
+ ORDER BY da.version
+ )
+ SELECT
+ chain.ver,
+ chain.doc_id,
+ CASE
+ WHEN chain.ver = 1 THEN chain.prev_version_hash IS NULL
+ ELSE chain.prev_version_hash IS NOT DISTINCT FROM chain.expected_prev_hash
+ END AS hash_valid
+ FROM chain
+ ORDER BY chain.ver;
+END;
+$function$;
+
+-- 4i. write_audit_log — comprehensive audit logging
+CREATE OR REPLACE FUNCTION public.write_audit_log()
+ RETURNS trigger
+ LANGUAGE plpgsql
+ SECURITY DEFINER
+AS $function$
+DECLARE
+ v_user_id uuid;
+ v_company_id uuid;
+ v_action text;
+ v_old_state jsonb;
+ v_new_state jsonb;
+ v_record_id uuid;
+ v_desc text;
+BEGIN
+ IF TG_OP = 'DELETE' THEN
+ v_old_state := to_jsonb(OLD);
+ v_new_state := NULL;
+ v_record_id := OLD.id;
+ v_user_id := (v_old_state->>'user_id')::uuid;
+ v_company_id := (v_old_state->>'company_id')::uuid;
+ v_action := 'DELETE';
+ v_desc := 'Deleted ' || TG_TABLE_NAME || ' record';
+ ELSIF TG_OP = 'INSERT' THEN
+ v_old_state := NULL;
+ v_new_state := to_jsonb(NEW);
+ v_record_id := NEW.id;
+ v_user_id := (v_new_state->>'user_id')::uuid;
+ v_company_id := (v_new_state->>'company_id')::uuid;
+ v_action := 'INSERT';
+ v_desc := 'Created ' || TG_TABLE_NAME || ' record';
+ ELSIF TG_OP = 'UPDATE' THEN
+ v_old_state := to_jsonb(OLD);
+ v_new_state := to_jsonb(NEW);
+ v_record_id := COALESCE(NEW.id, OLD.id);
+ v_user_id := COALESCE((v_new_state->>'user_id')::uuid, (v_old_state->>'user_id')::uuid);
+ v_company_id := COALESCE((v_new_state->>'company_id')::uuid, (v_old_state->>'company_id')::uuid);
+ v_action := 'UPDATE';
+ v_desc := 'Updated ' || TG_TABLE_NAME || ' record';
+
+ IF TG_TABLE_NAME = 'journal_entries' THEN
+ IF OLD.status = 'draft' AND NEW.status = 'posted' THEN
+ v_action := 'COMMIT';
+ v_desc := 'Committed journal entry ' || NEW.voucher_series || NEW.voucher_number;
+ ELSIF OLD.status = 'posted' AND NEW.status = 'reversed' THEN
+ v_action := 'REVERSE';
+ v_desc := 'Reversed journal entry ' || OLD.voucher_series || OLD.voucher_number;
+ END IF;
+ END IF;
+
+ IF TG_TABLE_NAME = 'fiscal_periods' THEN
+ IF (OLD.locked_at IS NULL AND NEW.locked_at IS NOT NULL) THEN
+ v_action := 'LOCK_PERIOD';
+ v_desc := 'Locked fiscal period "' || NEW.name || '"';
+ ELSIF (NOT OLD.is_closed AND NEW.is_closed) THEN
+ v_action := 'CLOSE_PERIOD';
+ v_desc := 'Closed fiscal period "' || NEW.name || '"';
+ END IF;
+ END IF;
+ END IF;
+
+ v_user_id := COALESCE(v_user_id, auth.uid());
+
+ INSERT INTO public.audit_log (user_id, company_id, action, table_name, record_id, actor_id, old_state, new_state, description)
+ VALUES (v_user_id, v_company_id, v_action, TG_TABLE_NAME, v_record_id, v_user_id, v_old_state, v_new_state, v_desc);
+
+ IF TG_OP = 'DELETE' THEN
+ RETURN OLD;
+ END IF;
+ RETURN NEW;
+END;
+$function$;
+
+-- 4j. delete_last_voucher — deletes only the last voucher in a series
+CREATE OR REPLACE FUNCTION public.delete_last_voucher(p_company_id uuid, p_entry_id uuid)
+ RETURNS jsonb
+ LANGUAGE plpgsql
+ SECURITY DEFINER
+ SET search_path TO 'public'
+AS $function$
+DECLARE
+ v_entry record;
+ v_period record;
+ v_max_voucher integer;
+ v_ref_count integer;
+ v_caller_role text;
+ v_snapshot jsonb;
+ v_lines_snapshot jsonb;
+BEGIN
+ SELECT cm.role INTO v_caller_role
+ FROM company_members cm
+ WHERE cm.company_id = p_company_id
+ AND cm.user_id = auth.uid();
+
+ IF v_caller_role IS NULL OR v_caller_role NOT IN ('owner', 'admin') THEN
+ RAISE EXCEPTION 'Only company owners and admins can delete vouchers';
+ END IF;
+
+ SELECT * INTO v_entry
+ FROM journal_entries
+ WHERE id = p_entry_id
+ AND company_id = p_company_id
+ FOR UPDATE;
+
+ IF v_entry IS NULL THEN
+ RAISE EXCEPTION 'Journal entry not found';
+ END IF;
+
+ IF v_entry.status != 'posted' THEN
+ RAISE EXCEPTION 'Only posted entries can be deleted (current status: %)', v_entry.status;
+ END IF;
+
+ SELECT * INTO v_period
+ FROM fiscal_periods
+ WHERE id = v_entry.fiscal_period_id
+ FOR UPDATE;
+
+ IF v_period.is_closed THEN
+ RAISE EXCEPTION 'Cannot delete voucher in a closed fiscal period';
+ END IF;
+
+ IF v_period.locked_at IS NOT NULL THEN
+ RAISE EXCEPTION 'Cannot delete voucher in a locked fiscal period';
+ END IF;
+
+ PERFORM 1 FROM voucher_sequences
+ WHERE company_id = p_company_id
+ AND fiscal_period_id = v_entry.fiscal_period_id
+ AND voucher_series = v_entry.voucher_series
+ FOR UPDATE;
+
+ SELECT MAX(voucher_number) INTO v_max_voucher
+ FROM journal_entries
+ WHERE company_id = p_company_id
+ AND fiscal_period_id = v_entry.fiscal_period_id
+ AND voucher_series = v_entry.voucher_series
+ AND status NOT IN ('cancelled', 'draft');
+
+ IF v_entry.voucher_number != v_max_voucher THEN
+ RAISE EXCEPTION 'Kan bara radera det sista verifikatet i serien. % har nummer % men senaste är %',
+ v_entry.voucher_series, v_entry.voucher_number, v_max_voucher;
+ END IF;
+
+ SELECT COUNT(*) INTO v_ref_count
+ FROM journal_entries
+ WHERE company_id = p_company_id
+ AND status != 'cancelled'
+ AND (reverses_id = p_entry_id OR correction_of_id = p_entry_id);
+
+ IF v_ref_count > 0 THEN
+ RAISE EXCEPTION 'Cannot delete: other entries reference this voucher (% references)',
+ v_ref_count;
+ END IF;
+
+ SELECT jsonb_agg(to_jsonb(l)) INTO v_lines_snapshot
+ FROM journal_entry_lines l
+ WHERE l.journal_entry_id = p_entry_id;
+
+ v_snapshot := to_jsonb(v_entry) || jsonb_build_object('lines', COALESCE(v_lines_snapshot, '[]'::jsonb));
+
+ IF v_entry.reverses_id IS NOT NULL THEN
+ PERFORM set_config('gnubok.allow_delete', 'true', true);
+ UPDATE journal_entries
+ SET status = 'posted', reversed_by_id = NULL
+ WHERE id = v_entry.reverses_id
+ AND company_id = p_company_id;
+ END IF;
+
+ PERFORM set_config('gnubok.allow_delete', 'true', true);
+
+ UPDATE document_attachments
+ SET journal_entry_id = NULL
+ WHERE journal_entry_id = p_entry_id;
+
+ DELETE FROM journal_entries WHERE id = p_entry_id;
+
+ UPDATE voucher_sequences
+ SET last_number = GREATEST(last_number - 1, 0)
+ WHERE company_id = p_company_id
+ AND fiscal_period_id = v_entry.fiscal_period_id
+ AND voucher_series = v_entry.voucher_series;
+
+ INSERT INTO audit_log (user_id, action, table_name, record_id, actor_id, old_state, description)
+ VALUES (
+ v_entry.user_id,
+ 'DELETE',
+ 'journal_entries',
+ p_entry_id,
+ auth.uid(),
+ v_snapshot,
+ 'Deleted voucher ' || v_entry.voucher_series || v_entry.voucher_number ||
+ ' (delete_last_voucher RPC, caller: ' || auth.uid() || ')'
+ );
+
+ RETURN jsonb_build_object(
+ 'deleted', true,
+ 'voucher_series', v_entry.voucher_series,
+ 'voucher_number', v_entry.voucher_number
+ );
+END;
+$function$;
+
+-- 4k. replace_sie_import — cancels entries from a SIE import
+CREATE OR REPLACE FUNCTION public.replace_sie_import(p_company_id uuid, p_import_id uuid)
+ RETURNS integer
+ LANGUAGE plpgsql
+ SECURITY DEFINER
+ SET search_path TO 'public'
+AS $function$
+DECLARE
+ v_cancelled integer;
+ v_fiscal_period_id uuid;
+ v_opening_balance_entry_id uuid;
+BEGIN
+ SELECT fiscal_period_id, opening_balance_entry_id
+ INTO v_fiscal_period_id, v_opening_balance_entry_id
+ FROM public.sie_imports
+ WHERE id = p_import_id AND company_id = p_company_id AND status = 'completed';
+
+ IF NOT FOUND THEN
+ RAISE EXCEPTION 'Import % not found or not in completed status', p_import_id;
+ END IF;
+
+ UPDATE public.journal_entries
+ SET status = 'cancelled'
+ WHERE company_id = p_company_id
+ AND status = 'posted'
+ AND id IN (
+ SELECT v_opening_balance_entry_id WHERE v_opening_balance_entry_id IS NOT NULL
+ UNION ALL
+ SELECT je.id FROM public.journal_entries je
+ WHERE je.company_id = p_company_id
+ AND je.fiscal_period_id = v_fiscal_period_id
+ AND je.source_type = 'import'
+ AND je.status = 'posted'
+ );
+ GET DIAGNOSTICS v_cancelled = ROW_COUNT;
+
+ UPDATE public.sie_imports
+ SET status = 'replaced', replaced_at = now()
+ WHERE id = p_import_id AND company_id = p_company_id;
+
+ RETURN v_cancelled;
+END;
+$function$;
+
+-- 4l. get_unlinked_1930_lines — bank reconciliation helper
+CREATE OR REPLACE FUNCTION public.get_unlinked_1930_lines(
+ p_company_id uuid,
+ p_date_from date DEFAULT NULL,
+ p_date_to date DEFAULT NULL
+)
+ RETURNS TABLE(
+ line_id uuid, journal_entry_id uuid, debit_amount numeric,
+ credit_amount numeric, line_description text, entry_date date,
+ voucher_number integer, voucher_series text, entry_description text,
+ source_type text
+ )
+ LANGUAGE sql
+ STABLE SECURITY DEFINER
+ SET search_path TO 'public'
+AS $function$
+ SELECT
+ jel.id AS line_id,
+ je.id AS journal_entry_id,
+ jel.debit_amount,
+ jel.credit_amount,
+ jel.line_description,
+ je.entry_date,
+ je.voucher_number,
+ je.voucher_series,
+ je.description AS entry_description,
+ je.source_type
+ FROM public.journal_entry_lines jel
+ JOIN public.journal_entries je ON je.id = jel.journal_entry_id
+ WHERE jel.account_number = '1930'
+ AND je.company_id = p_company_id
+ AND je.status = 'posted'
+ AND (p_date_from IS NULL OR je.entry_date >= p_date_from)
+ AND (p_date_to IS NULL OR je.entry_date <= p_date_to)
+ AND NOT EXISTS (
+ SELECT 1
+ FROM public.transactions t
+ WHERE t.journal_entry_id = je.id
+ AND t.company_id = p_company_id
+ )
+ ORDER BY je.entry_date, je.voucher_number;
+$function$;
+
+-- 4m. delete_user_account — full account deletion with cascade blockers
+CREATE OR REPLACE FUNCTION public.delete_user_account(target_user_id uuid)
+ RETURNS void
+ LANGUAGE plpgsql
+ SECURITY DEFINER
+ SET search_path TO 'public'
+ SET statement_timeout TO '60s'
+ SET lock_timeout TO '10s'
+AS $function$
+DECLARE
+ v_company_ids uuid[];
+BEGIN
+ IF auth.uid() IS DISTINCT FROM target_user_id THEN
+ RAISE EXCEPTION 'Can only delete your own account';
+ END IF;
+
+ SELECT array_agg(id) INTO v_company_ids
+ FROM public.companies
+ WHERE created_by = target_user_id;
+
+ DELETE FROM public.user_preferences WHERE user_id = target_user_id;
+ DELETE FROM public.extension_data WHERE user_id = target_user_id;
+
+ ALTER TABLE audit_log DISABLE TRIGGER audit_log_no_delete;
+ ALTER TABLE payment_match_log DISABLE TRIGGER payment_match_log_no_delete;
+ ALTER TABLE document_attachments DISABLE TRIGGER block_document_deletion;
+ ALTER TABLE journal_entries DISABLE TRIGGER enforce_journal_entry_immutability;
+ ALTER TABLE journal_entries DISABLE TRIGGER enforce_retention_journal_entries;
+ ALTER TABLE journal_entry_lines DISABLE TRIGGER enforce_journal_entry_line_immutability;
+
+ ALTER TABLE api_keys DISABLE TRIGGER audit_api_keys;
+ ALTER TABLE chart_of_accounts DISABLE TRIGGER audit_chart_of_accounts;
+ ALTER TABLE company_settings DISABLE TRIGGER audit_company_settings;
+ ALTER TABLE document_attachments DISABLE TRIGGER audit_document_attachments;
+ ALTER TABLE extension_data DISABLE TRIGGER audit_extension_data;
+ ALTER TABLE fiscal_periods DISABLE TRIGGER audit_fiscal_periods;
+ ALTER TABLE journal_entries DISABLE TRIGGER audit_journal_entries;
+ ALTER TABLE supplier_invoices DISABLE TRIGGER audit_supplier_invoices;
+
+ IF v_company_ids IS NOT NULL THEN
+ DELETE FROM public.audit_log
+ WHERE company_id = ANY(v_company_ids);
+
+ UPDATE public.fiscal_periods
+ SET previous_period_id = NULL,
+ closing_entry_id = NULL,
+ opening_balance_entry_id = NULL
+ WHERE company_id = ANY(v_company_ids);
+ END IF;
+
+ DELETE FROM auth.users WHERE id = target_user_id;
+
+ ALTER TABLE audit_log ENABLE TRIGGER audit_log_no_delete;
+ ALTER TABLE payment_match_log ENABLE TRIGGER payment_match_log_no_delete;
+ ALTER TABLE document_attachments ENABLE TRIGGER block_document_deletion;
+ ALTER TABLE journal_entries ENABLE TRIGGER enforce_journal_entry_immutability;
+ ALTER TABLE journal_entries ENABLE TRIGGER enforce_retention_journal_entries;
+ ALTER TABLE journal_entry_lines ENABLE TRIGGER enforce_journal_entry_line_immutability;
+ ALTER TABLE api_keys ENABLE TRIGGER audit_api_keys;
+ ALTER TABLE chart_of_accounts ENABLE TRIGGER audit_chart_of_accounts;
+ ALTER TABLE company_settings ENABLE TRIGGER audit_company_settings;
+ ALTER TABLE document_attachments ENABLE TRIGGER audit_document_attachments;
+ ALTER TABLE extension_data ENABLE TRIGGER audit_extension_data;
+ ALTER TABLE fiscal_periods ENABLE TRIGGER audit_fiscal_periods;
+ ALTER TABLE journal_entries ENABLE TRIGGER audit_journal_entries;
+ ALTER TABLE supplier_invoices ENABLE TRIGGER audit_supplier_invoices;
+END;
+$function$;
+
+
+-- ============================================================================
+-- 5. TRIGGERS on existing tables
+-- ============================================================================
+
+-- Document metadata immutability enforcement
+DROP TRIGGER IF EXISTS enforce_document_metadata_immutability ON public.document_attachments;
+CREATE TRIGGER enforce_document_metadata_immutability
+ BEFORE UPDATE ON public.document_attachments
+ FOR EACH ROW EXECUTE FUNCTION public.enforce_document_metadata_immutability();
+
+
+-- ============================================================================
+-- 6. MISSING DELETE POLICIES on existing tables
+-- ============================================================================
+
+DO $$
+DECLARE
+ r record;
+BEGIN
+ -- Tables with a direct company_id column
+ FOR r IN
+ SELECT unnest(ARRAY[
+ 'api_keys', 'bank_connections', 'bank_file_imports', 'calendar_feeds',
+ 'categorization_templates', 'chart_of_accounts', 'company_settings',
+ 'cost_centers', 'customers', 'deadlines', 'document_attachments',
+ 'extension_data', 'fiscal_periods',
+ 'invoice_inbox_items', 'invoice_payments',
+ 'invoice_reminders', 'invoices', 'journal_entries',
+ 'mapping_rules', 'projects',
+ 'receipts',
+ 'sie_account_mappings', 'sie_imports', 'skatteverket_tokens',
+ 'supplier_invoice_payments',
+ 'supplier_invoices', 'suppliers', 'transactions'
+ ]) AS tbl
+ LOOP
+ IF NOT EXISTS (
+ SELECT 1 FROM pg_policies
+ WHERE schemaname = 'public' AND tablename = r.tbl AND cmd = 'DELETE'
+ ) THEN
+ EXECUTE format(
+ 'CREATE POLICY %I ON public.%I FOR DELETE USING (company_id IN (SELECT user_company_ids()))',
+ r.tbl || '_delete', r.tbl
+ );
+ END IF;
+ END LOOP;
+
+ -- Tables that join through a parent to reach company_id
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='invoice_items' AND cmd='DELETE') THEN
+ CREATE POLICY invoice_items_delete ON public.invoice_items FOR DELETE
+ USING (invoice_id IN (SELECT id FROM invoices WHERE company_id IN (SELECT user_company_ids())));
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='journal_entry_lines' AND cmd='DELETE') THEN
+ CREATE POLICY journal_entry_lines_delete ON public.journal_entry_lines FOR DELETE
+ USING (journal_entry_id IN (SELECT id FROM journal_entries WHERE company_id IN (SELECT user_company_ids())));
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='receipt_line_items' AND cmd='DELETE') THEN
+ CREATE POLICY receipt_line_items_delete ON public.receipt_line_items FOR DELETE
+ USING (receipt_id IN (SELECT id FROM receipts WHERE company_id IN (SELECT user_company_ids())));
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='supplier_invoice_items' AND cmd='DELETE') THEN
+ CREATE POLICY supplier_invoice_items_delete ON public.supplier_invoice_items FOR DELETE
+ USING (supplier_invoice_id IN (SELECT id FROM supplier_invoices WHERE company_id IN (SELECT user_company_ids())));
+ END IF;
+
+ -- User-scoped tables (no company_id — use auth.uid())
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='extension_toggles' AND cmd='DELETE') THEN
+ CREATE POLICY extension_toggles_delete ON public.extension_toggles FOR DELETE
+ USING (auth.uid() = user_id);
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='notification_settings' AND cmd='DELETE') THEN
+ CREATE POLICY notification_settings_delete ON public.notification_settings FOR DELETE
+ USING (auth.uid() = user_id);
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='push_subscriptions' AND cmd='DELETE') THEN
+ CREATE POLICY push_subscriptions_delete ON public.push_subscriptions FOR DELETE
+ USING (auth.uid() = user_id);
+ END IF;
+
+ -- Tables that use other policy patterns
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='chat_sessions' AND cmd='DELETE') THEN
+ CREATE POLICY chat_sessions_delete ON public.chat_sessions FOR DELETE
+ USING (company_id IN (SELECT user_company_ids()));
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='chat_messages' AND cmd='DELETE') THEN
+ CREATE POLICY chat_messages_delete ON public.chat_messages FOR DELETE
+ USING (company_id IN (SELECT user_company_ids()));
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='company_members' AND cmd='DELETE') THEN
+ CREATE POLICY company_members_delete ON public.company_members FOR DELETE
+ USING (company_id IN (SELECT user_company_ids()));
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='company_invitations' AND cmd='DELETE') THEN
+ CREATE POLICY company_invitations_delete ON public.company_invitations FOR DELETE
+ USING (company_id IN (SELECT user_company_ids()));
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='team_invitations' AND cmd='DELETE') THEN
+ CREATE POLICY team_invitations_delete ON public.team_invitations FOR DELETE
+ USING (team_id IN (SELECT user_team_ids()));
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='team_members' AND cmd='DELETE') THEN
+ CREATE POLICY team_members_delete ON public.team_members FOR DELETE
+ USING (team_id IN (SELECT user_team_ids()));
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='provider_consents' AND cmd='DELETE') THEN
+ CREATE POLICY provider_consents_delete ON public.provider_consents FOR DELETE
+ USING (company_id IN (SELECT user_company_ids()));
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='provider_consent_tokens' AND cmd='DELETE') THEN
+ CREATE POLICY provider_consent_tokens_delete ON public.provider_consent_tokens FOR DELETE
+ USING (consent_id IN (SELECT id FROM provider_consents WHERE company_id IN (SELECT user_company_ids())));
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_policies WHERE schemaname='public' AND tablename='provider_otc' AND cmd='DELETE') THEN
+ CREATE POLICY provider_otc_delete ON public.provider_otc FOR DELETE
+ USING (consent_id IN (SELECT id FROM provider_consents WHERE company_id IN (SELECT user_company_ids())));
+ END IF;
+END $$;
+
+
+-- ============================================================================
+-- 7. NOTIFY PostgREST to reload schema
+-- ============================================================================
+
+NOTIFY pgrst, 'reload schema';