Bug/accounting ps eu (#474)

* feat(api): implement commit functionality for journal entries

* fix(extensions): make ExtensionSettings.clear() a real delete so disconnect flows work

The 2026-03-30 multi-tenant refactor dropped all RLS policies on extension_data
and recreated only SELECT/INSERT/UPDATE. Combined with `value jsonb NOT NULL`,
every extension that called `settings.set(key, null)` to clear stored state
(cloud-backup disconnect, skatteverket OAuth/AGI cleanup, arcim-migration
consent reset) silently failed — the upsert hit the NOT NULL constraint and
the error was swallowed, leaving users stuck with stale connection rows.

Adds an `extension_data_delete` RLS policy, a `clear(key)` method backed by a
real DELETE, switches the four affected handlers, and makes `set()` throw on
Supabase error so this class of silent failure can't recur.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(journal-entries): add draft saving functionality to journal entry form

* feat: add periodisk sammanställning report generation and CSV export

- Implemented period date helpers in `period-dates.ts` for calculating start and end dates based on period type (monthly, quarterly, yearly).
- Created `periodisk-sammanstallning.ts` to generate the periodisk sammanställning report, including data fetching, validation, and warning handling.
- Developed CSV serializer in `periodisk-sammanstallning-csv.ts` for exporting the report in SKV574008 format.
- Added new columns to `company_settings` for storing periodisk sammanställning settings and tax contact information via migration.
- Introduced a new migration to add a `paid_with_private_funds` flag to `supplier_invoices` for tracking out-of-pocket expenses.
- Updated journal entries to include the new source type for privately paid supplier invoices.

* feat(migrations): add paid_with_private_funds flag to supplier_invoices and expand journal_entries.source_type CHECK

* fix(ai_requests): drop existing policies and trigger before creating new ones

* fix(migrations): ensure extension_data has a proper DELETE policy for ExtensionSettings.clear()

* fix(supplier-invoices): update error handling for invalid input in POST request

* fix: correct capitalization in project title

* fix(migrations): resolve duplicate version 20260513120000

Two migrations shared the same timestamp prefix, causing
schema_migrations_pkey collision on Supabase preview branches.
Bump extension_data_delete_policy to 20260513120001.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-05-14 01:10:44 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent ba45b8f661
commit c8461397c8
40 changed files with 3147 additions and 55 deletions
@@ -120,6 +120,34 @@ describe('createExtensionContext', () => {
expect(supabase.from).toHaveBeenCalledWith('extension_data')
})
it('settings.set() throws when supabase returns an error', async () => {
const { supabase, mockResult } = createMockSupabase()
mockResult({ data: null, error: { message: 'null value in column "value" violates not-null constraint' } })
const ctx = createExtensionContext(supabase as never, 'user-1', 'company-1', 'test-ext')
await expect(ctx.settings.set('my-key', null)).rejects.toThrow(/extension_data set failed/)
})
it('settings.clear() deletes from extension_data table', async () => {
const { supabase, mockResult } = createMockSupabase()
mockResult({ data: null, error: null })
const ctx = createExtensionContext(supabase as never, 'user-1', 'company-1', 'test-ext')
await ctx.settings.clear('my-key')
expect(supabase.from).toHaveBeenCalledWith('extension_data')
})
it('settings.clear() throws when supabase returns an error', async () => {
const { supabase, mockResult } = createMockSupabase()
mockResult({ data: null, error: { message: 'permission denied' } })
const ctx = createExtensionContext(supabase as never, 'user-1', 'company-1', 'test-ext')
await expect(ctx.settings.clear('my-key')).rejects.toThrow(/extension_data clear failed/)
})
it('storage.getPublicUrl() returns URL string', () => {
const { supabase } = createMockSupabase()
const ctx = createExtensionContext(supabase as never, 'user-1', 'company-1', 'test-ext')
+16 -1
View File
@@ -50,7 +50,7 @@ function createSettings(
},
async set<T>(key: string, value: T): Promise<void> {
await supabase
const { error } = await supabase
.from('extension_data')
.upsert(
{
@@ -62,6 +62,21 @@ function createSettings(
},
{ onConflict: 'company_id,extension_id,key' }
)
if (error) {
throw new Error(`extension_data set failed for ${extensionId}/${key}: ${error.message}`)
}
},
async clear(key: string): Promise<void> {
const { error } = await supabase
.from('extension_data')
.delete()
.eq('company_id', companyId)
.eq('extension_id', extensionId)
.eq('key', key)
if (error) {
throw new Error(`extension_data clear failed for ${extensionId}/${key}: ${error.message}`)
}
},
}
}
+6
View File
@@ -149,6 +149,12 @@ export interface ExtensionLogger {
export interface ExtensionSettings {
get<T>(key?: string): Promise<T | null>
set<T>(key: string, value: T): Promise<void>
/**
* Remove a stored key. Use this to clear state instead of `set(key, null)`,
* which fails against the `value jsonb NOT NULL` constraint on extension_data.
* No-op when the key does not exist.
*/
clear(key: string): Promise<void>
}
/** Storage accessor wrapping Supabase storage */