From 6486e0d9e2d2cc151627ae3f8b6c51d5fb1a62f5 Mon Sep 17 00:00:00 2001 From: Mattsson <111893710+mattssonn@users.noreply.github.com> Date: Wed, 8 Apr 2026 11:25:46 +0200 Subject: [PATCH] Fix/transaction inconsitensies (#190) * fix: request explicit page size from Enable Banking API to fetch all transactions The API defaults to ~10 transactions per page when no limit is specified, causing incomplete syncs for users with more transactions. * fix: enhance DELETE operations and add missing RLS policies for multi-tenant support --- app/api/customers/[id]/route.ts | 8 +- app/api/suppliers/[id]/route.ts | 8 +- .../general/ArcimMigrationWorkspace.tsx | 78 +++++++--- extensions/general/arcim-migration/index.ts | 8 ++ .../lib/migration-orchestrator.ts | 49 ++++--- extensions/general/arcim-migration/types.ts | 15 +- .../general/enable-banking/lib/api-client.ts | 3 + lib/providers/bokio/config.ts | 16 +++ lib/providers/bokio/mapper.ts | 106 ++++++++++++++ ...0408120000_add_missing_delete_policies.sql | 135 ++++++++++++++++++ 10 files changed, 381 insertions(+), 45 deletions(-) create mode 100644 supabase/migrations/20260408120000_add_missing_delete_policies.sql diff --git a/app/api/customers/[id]/route.ts b/app/api/customers/[id]/route.ts index 4a9a7974..957ebbaf 100644 --- a/app/api/customers/[id]/route.ts +++ b/app/api/customers/[id]/route.ts @@ -174,9 +174,9 @@ export async function DELETE( const companyId = await requireCompanyId(supabase, user.id) - const { error } = await supabase + const { error, count } = await supabase .from('customers') - .delete() + .delete({ count: 'exact' }) .eq('id', id) .eq('company_id', companyId) @@ -184,5 +184,9 @@ export async function DELETE( return NextResponse.json({ error: error.message }, { status: 500 }) } + if (count === 0) { + return NextResponse.json({ error: 'Customer not found' }, { status: 404 }) + } + return NextResponse.json({ success: true }) } diff --git a/app/api/suppliers/[id]/route.ts b/app/api/suppliers/[id]/route.ts index c4f9ab6c..9009f918 100644 --- a/app/api/suppliers/[id]/route.ts +++ b/app/api/suppliers/[id]/route.ts @@ -141,9 +141,9 @@ export async function DELETE( ) } - const { error } = await supabase + const { error, count: deleteCount } = await supabase .from('suppliers') - .delete() + .delete({ count: 'exact' }) .eq('id', id) .eq('company_id', companyId) @@ -151,5 +151,9 @@ export async function DELETE( return NextResponse.json({ error: error.message }, { status: 500 }) } + if (deleteCount === 0) { + return NextResponse.json({ error: 'Supplier not found' }, { status: 404 }) + } + return NextResponse.json({ success: true }) } diff --git a/components/extensions/general/ArcimMigrationWorkspace.tsx b/components/extensions/general/ArcimMigrationWorkspace.tsx index 1450bba8..e6d741cd 100644 --- a/components/extensions/general/ArcimMigrationWorkspace.tsx +++ b/components/extensions/general/ArcimMigrationWorkspace.tsx @@ -44,12 +44,19 @@ const ARCIM_PROVIDERS: { id: ArcimProvider; name: string; authType: 'oauth' | 't { id: 'briox', name: 'Briox', authType: 'token' }, ] +interface SkipReasons { + duplicate?: number + inactive?: number + failed?: number + noMatch?: number +} + interface MigrationResults { companyInfo?: { imported: boolean } - customers?: { total: number; imported: number; skipped: number } - suppliers?: { total: number; imported: number; skipped: number } - salesInvoices?: { total: number; imported: number; skipped: number } - supplierInvoices?: { total: number; imported: number; skipped: number } + customers?: { total: number; imported: number; skipped: number; skipReasons?: SkipReasons } + suppliers?: { total: number; imported: number; skipped: number; skipReasons?: SkipReasons } + salesInvoices?: { total: number; imported: number; skipped: number; skipReasons?: SkipReasons } + supplierInvoices?: { total: number; imported: number; skipped: number; skipReasons?: SkipReasons } } import AccountMappingStep from '@/components/import/AccountMappingStep' import type { AccountMapping, ImportResult, ParsedSIEFile } from '@/lib/import/types' @@ -120,6 +127,7 @@ interface PreviewData { transactionCount: number fiscalYears: number[] } | null + hasSieData: boolean } interface SIEFileStatus { @@ -476,6 +484,7 @@ function ConnectStep({ setApiToken(e.target.value)} @@ -489,6 +498,7 @@ function ConnectStep({ setCompanyId(e.target.value)} @@ -584,17 +594,36 @@ function PreviewStep({ )} - {preview && !preview.sieAvailable && !isLoading && ( -
- + {preview && !preview.sieAvailable && !isLoading && preview.hasSieData && ( +
+
-

SIE-hämtning inte tillgänglig

+

SIE-data redan importerad

- SIE-hämtning är inte tillgänglig för denna leverantör ännu. Du kan importera SIE-filen manuellt via SIE-importen. + Bokföringsdata har redan importerats via SIE-fil. Du kan fortsätta med att importera kunder, leverantörer och fakturor.

)} + + {preview && !preview.sieAvailable && !isLoading && !preview.hasSieData && ( +
+ +
+

SIE-import krävs

+

+ Bokföringsdata (kontoplan, verifikationer och balanser) måste importeras via SIE-fil innan kunder, leverantörer och fakturor kan hämtas. Exportera en SIE-fil från {ARCIM_PROVIDERS.find(p => p.id === preview.consent.provider)?.name ?? 'ditt bokföringssystem'} och ladda upp den i gnubok. +

+ +
+
+ )} @@ -603,7 +632,7 @@ function PreviewStep({ Tillbaka - @@ -817,15 +846,15 @@ function OptionsStep({ /> } - label="Kundfakturor (öppna)" - description="Obetalda kundfakturor" + label="Kundfakturor" + description="Alla kundfakturor (betalda och obetalda)" checked={options.importSalesInvoices} onChange={() => toggleOption('importSalesInvoices')} /> } - label="Leverantörsfakturor (öppna)" - description="Obetalda leverantörsfakturor" + label="Leverantörsfakturor" + description="Alla leverantörsfakturor (betalda och obetalda)" checked={options.importSupplierInvoices} onChange={() => toggleOption('importSupplierInvoices')} /> @@ -1292,7 +1321,7 @@ function ResultStep({ label="Kunder" status="success" statusText={`${results.customers!.imported} importerade`} - detail={results.customers!.skipped > 0 ? `${results.customers!.skipped} fanns redan` : undefined} + detail={results.customers!.skipped > 0 ? formatSkipReasons(results.customers!.skipReasons, 'customer') ?? `${results.customers!.skipped} hoppades över` : undefined} /> )} {hasSuppliers && ( @@ -1301,7 +1330,7 @@ function ResultStep({ label="Leverantörer" status="success" statusText={`${results.suppliers!.imported} importerade`} - detail={results.suppliers!.skipped > 0 ? `${results.suppliers!.skipped} fanns redan` : undefined} + detail={results.suppliers!.skipped > 0 ? formatSkipReasons(results.suppliers!.skipReasons, 'supplier') ?? `${results.suppliers!.skipped} hoppades över` : undefined} /> )} {hasSalesInvoices && ( @@ -1310,7 +1339,7 @@ function ResultStep({ label="Kundfakturor" status="success" statusText={`${results.salesInvoices!.imported} importerade`} - detail={results.salesInvoices!.skipped > 0 ? `${results.salesInvoices!.skipped} hoppades över` : undefined} + detail={results.salesInvoices!.skipped > 0 ? formatSkipReasons(results.salesInvoices!.skipReasons, 'invoice') ?? `${results.salesInvoices!.skipped} hoppades över` : undefined} /> )} {hasSupplierInvoices && ( @@ -1319,7 +1348,7 @@ function ResultStep({ label="Leverantörsfakturor" status="success" statusText={`${results.supplierInvoices!.imported} importerade`} - detail={results.supplierInvoices!.skipped > 0 ? `${results.supplierInvoices!.skipped} hoppades över` : undefined} + detail={results.supplierInvoices!.skipped > 0 ? formatSkipReasons(results.supplierInvoices!.skipReasons, 'invoice') ?? `${results.supplierInvoices!.skipped} hoppades över` : undefined} /> )}
@@ -1387,6 +1416,19 @@ function ResultStep({ ) } +function formatSkipReasons(reasons?: SkipReasons, entityType?: 'customer' | 'supplier' | 'invoice'): string | undefined { + if (!reasons) return undefined + const parts: string[] = [] + if (reasons.duplicate) parts.push(`${reasons.duplicate} fanns redan`) + if (reasons.inactive) parts.push(`${reasons.inactive} inaktiv${reasons.inactive > 1 ? 'a' : ''}`) + if (reasons.noMatch) { + const matchLabel = entityType === 'invoice' ? 'utan matchning' : 'utan matchning' + parts.push(`${reasons.noMatch} ${matchLabel}`) + } + if (reasons.failed) parts.push(`${reasons.failed} misslyckades`) + return parts.length > 0 ? parts.join(', ') : undefined +} + /** Simple row for non-SIE entity results (customers, invoices, etc.) */ function EntityResultRow({ icon, diff --git a/extensions/general/arcim-migration/index.ts b/extensions/general/arcim-migration/index.ts index 0cb43c0a..bbb92337 100644 --- a/extensions/general/arcim-migration/index.ts +++ b/extensions/general/arcim-migration/index.ts @@ -452,6 +452,13 @@ export const arcimMigrationExtension: Extension = { } } + // Check if the company already has completed SIE imports (from manual upload) + const { count: sieImportCount } = await supabase + .from('sie_imports') + .select('*', { count: 'exact', head: true }) + .eq('company_id', companyId) + .eq('status', 'completed') + return NextResponse.json({ consent: { id: consent.id, @@ -462,6 +469,7 @@ export const arcimMigrationExtension: Extension = { companyInfo: mapped, sieAvailable, sieStats, + hasSieData: (sieImportCount ?? 0) > 0, }) } catch (error) { log.error('Preview error:', error) diff --git a/extensions/general/arcim-migration/lib/migration-orchestrator.ts b/extensions/general/arcim-migration/lib/migration-orchestrator.ts index 2817b547..81537414 100644 --- a/extensions/general/arcim-migration/lib/migration-orchestrator.ts +++ b/extensions/general/arcim-migration/lib/migration-orchestrator.ts @@ -8,12 +8,12 @@ * 1. Company info → pre-fill company_settings * 2. Customers → needed before sales invoices * 3. Suppliers → needed before supplier invoices - * 4. Sales invoices (open only) - * 5. Supplier invoices (open only) + * 4. Sales invoices (all statuses, duplicates skipped) + * 5. Supplier invoices (all statuses, duplicates skipped) */ import type { SupabaseClient } from '@supabase/supabase-js' -import type { MigrationProgress, MigrationResults } from '../types' +import type { MigrationProgress, MigrationResults, SkipReasons } from '../types' import type { ProviderName } from '@/lib/providers/types' import { resolveConsent } from '@/lib/providers/resolve-consent' import { @@ -111,10 +111,12 @@ export async function executeMigration(options: MigrationOptions): Promise 0) { console.log(`[migration] Customer skipped (duplicate org_number ${orgNumber}): ${customer.party.name}`) customerIdMap.set(customer.id, existing[0].id) + skipReasons.duplicate = (skipReasons.duplicate ?? 0) + 1 skipped++ continue } @@ -146,6 +149,7 @@ export async function executeMigration(options: MigrationOptions): Promise 0) { console.log(`[migration] Supplier skipped (duplicate org_number ${orgNumber}): ${supplier.party.name}`) supplierIdMap.set(supplier.id, existing[0].id) + skipReasons.duplicate = (skipReasons.duplicate ?? 0) + 1 skipped++ continue } @@ -203,6 +210,7 @@ export async function executeMigration(options: MigrationOptions): Promise - i.status === 'sent' || i.status === 'overdue' || i.status === 'booked' - ) - console.log(`[migration] Sales invoices: ${invoices.length} total, ${openInvoices.length} open (filtered by status: sent/overdue/booked)`) + console.log(`[migration] Sales invoices: ${invoices.length} total`) let imported = 0 let skipped = 0 + const skipReasons: SkipReasons = {} - for (const inv of openInvoices) { + for (const inv of invoices) { const customerOrgNumber = inv.customer.legalEntity?.companyId || inv.customer.identifications?.find(i => i.schemeId === 'SE:ORGNR')?.id @@ -280,6 +286,7 @@ export async function executeMigration(options: MigrationOptions): Promise 0) { console.log(`[migration] Sales invoice ${inv.invoiceNumber} skipped — already exists`) + skipReasons.duplicate = (skipReasons.duplicate ?? 0) + 1 skipped++ continue } @@ -307,6 +315,7 @@ export async function executeMigration(options: MigrationOptions): Promise - i.status === 'sent' || i.status === 'overdue' || i.status === 'booked' || i.status === 'draft' - ) - console.log(`[migration] Supplier invoices: ${invoices.length} total, ${openInvoices.length} open (filtered by status: sent/overdue/booked/draft)`) + console.log(`[migration] Supplier invoices: ${invoices.length} total`) let imported = 0 let skipped = 0 + const skipReasons: SkipReasons = {} - for (const inv of openInvoices) { + for (const inv of invoices) { const supplierOrgNumber = inv.supplier.legalEntity?.companyId || inv.supplier.identifications?.find(i => i.schemeId === 'SE:ORGNR')?.id @@ -392,6 +399,7 @@ export async function executeMigration(options: MigrationOptions): Promise 0) { console.log(`[migration] Supplier invoice ${inv.invoiceNumber} skipped — already exists for supplier "${inv.supplier.name}"`) + skipReasons.duplicate = (skipReasons.duplicate ?? 0) + 1 skipped++ continue } @@ -418,6 +427,7 @@ export async function executeMigration(options: MigrationOptions): Promise): Accou }; } +/** + * Map Bokio Supplier to SupplierDto. + * + * Bokio Supplier fields: + * - id, name, orgNumber, vatNumber, paymentTerms + * - address: { line1, line2, city, postalCode, country } + * - contactsDetails: [{ email, phone, name }] + * - bankAccount, bankgiro, plusgiro + */ +export function mapBokioToSupplier(raw: Record): SupplierDto { + const name = (raw['name'] as string) ?? ''; + const orgNumber = raw['orgNumber'] as string | undefined; + const address = raw['address'] as Record | undefined; + const contacts = (raw['contactsDetails'] as Record[] | undefined) ?? []; + const firstContact = contacts[0]; + + const party = buildParty(name, orgNumber, address); + if (firstContact) { + party.contact = { + email: firstContact['email'] as string | undefined, + telephone: firstContact['phone'] as string | undefined, + name: firstContact['name'] as string | undefined, + }; + } + + return { + id: String(raw['id'] ?? ''), + supplierNumber: String(raw['id'] ?? ''), + party, + active: true, + vatNumber: raw['vatNumber'] as string | undefined, + bankAccount: raw['bankAccount'] as string | undefined, + bankGiro: raw['bankgiro'] as string | undefined, + plusGiro: raw['plusgiro'] as string | undefined, + defaultPaymentTermsDays: raw['paymentTerms'] != null ? Number(raw['paymentTerms']) : undefined, + _raw: raw, + }; +} + +/** + * Map Bokio Supplier Invoice to SupplierInvoiceDto. + * + * Bokio Supplier Invoice fields: + * - id, invoiceNumber, status (draft|published|paid|overdue|cancelled) + * - invoiceDate, dueDate, currency, totalAmount, totalTax, paidAmount + * - supplierRef: { id, name }, lineItems: [{ id, description, quantity, unitPrice, taxRate, unitType }] + * - ocrNumber + */ +export function mapBokioToSupplierInvoice(raw: Record): SupplierInvoiceDto { + const currency = (raw['currency'] as string) ?? 'SEK'; + const totalAmount = (raw['totalAmount'] as number) ?? 0; + const totalTax = (raw['totalTax'] as number) ?? 0; + const paidAmount = (raw['paidAmount'] as number) ?? 0; + const balance = totalAmount - paidAmount; + + const supplierRef = raw['supplierRef'] as Record | undefined; + const rawLines = (raw['lineItems'] as Record[] | undefined) ?? []; + + const lines: SupplierInvoiceLineDto[] = rawLines.map((line, idx) => { + const unitPrice = line['unitPrice'] as number | undefined; + const quantity = line['quantity'] as number | undefined; + const lineTotal = unitPrice != null && quantity != null ? unitPrice * quantity : 0; + + return { + id: String(line['id'] ?? idx + 1), + description: line['description'] as string | undefined, + quantity, + unitCode: line['unitType'] as string | undefined, + unitPrice: unitPrice != null ? amount(unitPrice, currency) : undefined, + lineExtensionAmount: amount(lineTotal, currency), + taxPercent: line['taxRate'] as number | undefined, + }; + }); + + const legalMonetaryTotal: LegalMonetaryTotalDto = { + lineExtensionAmount: amount(totalAmount - totalTax, currency), + taxInclusiveAmount: amount(totalAmount, currency), + payableAmount: amount(totalAmount, currency), + }; + + const paymentStatus: PaymentStatusDto = { + paid: paidAmount >= totalAmount && totalAmount > 0, + balance: amount(balance, currency), + }; + + return { + id: String(raw['id'] ?? ''), + invoiceNumber: String(raw['invoiceNumber'] ?? raw['id'] ?? ''), + issueDate: (raw['invoiceDate'] as string) ?? '', + dueDate: raw['dueDate'] as string | undefined, + currencyCode: currency, + status: deriveInvoiceStatus(raw), + supplier: buildParty( + (supplierRef?.['name'] as string) ?? '', + ), + buyer: buildParty(''), + lines, + legalMonetaryTotal, + paymentStatus, + ocrNumber: raw['ocrNumber'] as string | undefined, + _raw: raw, + }; +} + /** * Map Bokio Company to CompanyInformationDto. * diff --git a/supabase/migrations/20260408120000_add_missing_delete_policies.sql b/supabase/migrations/20260408120000_add_missing_delete_policies.sql new file mode 100644 index 00000000..804861d9 --- /dev/null +++ b/supabase/migrations/20260408120000_add_missing_delete_policies.sql @@ -0,0 +1,135 @@ +-- ============================================================================= +-- 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())) + );