From d484c341a4ddd8c286c857afc91b5ecc699a185f Mon Sep 17 00:00:00 2001 From: Mattsson <111893710+mattssonn@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:48:08 +0200 Subject: [PATCH] Psu type configuration (#248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implement salary module with personnummer encryption, salary entries, tax tables, and AGI tracking - Added personnummer encryption and decryption functions for secure storage. - Created salary entries handling for journal entries including gross salary, tax withholding, and employer contributions. - Implemented tax table lookup functionality for calculating tax amounts based on monthly income. - Developed SQL migration for salary module including tables for payroll configuration, tax rates, employees, salary runs, salary run employees, salary line items, and AGI declarations. - Established row-level security policies for all new tables to ensure company-scoped access. * feat: add salary calculation modules for 2026 - Implemented engångsskatt calculation for one-time payments with tax brackets. - Added löneväxling functionality for salary sacrifice to pension, including employer savings and warnings. - Created pain.001 generator for salary batch payments in compliance with Swedish banking standards. - Developed PDF template for payslips, including detailed breakdowns and employer costs. - Generated seed data for Swedish tax tables for 2026, including SQL insert statements. - Implemented traktamente calculations for per diem and mileage allowances, adhering to Skatteverket regulations. - Added seed script for populating tax tables in the database. * feat: Update meal reduction percentages in traktamente calculation fix: Remove obsolete seed script for 2026 tax tables feat: Extend SalaryRunStatus type to include 'corrected' status feat: Implement KU10 XML generation endpoint for annual employee income statements feat: Add endpoint for creating corrections to booked salary runs feat: Implement endpoint for sending payslip PDFs to employees feat: Create KU10 XML generator for annual reporting feat: Add salary transaction matcher for auto-linking bank transactions to salary entries chore: Add database migration for salary correction support * feat: replace select elements with custom Select component for employment and salary types * feat: enhance salary calculations with pension entry and avgifter category support * feat: enhance banking settings with PSU type detection and error handling * fix: improve error handling for bank connection and update access denial message --- app/(dashboard)/settings/banking/page.tsx | 20 +++++++- .../enable-banking/callback/route.ts | 8 +++ .../components/BankSelector.tsx | 8 ++- .../components/BankingSettingsPanel.tsx | 50 +++++++++++++++++-- extensions/general/enable-banking/index.ts | 37 +++++++++++--- .../general/enable-banking/lib/api-client.ts | 8 +-- 6 files changed, 114 insertions(+), 17 deletions(-) diff --git a/app/(dashboard)/settings/banking/page.tsx b/app/(dashboard)/settings/banking/page.tsx index e9b8b71c..21243a49 100644 --- a/app/(dashboard)/settings/banking/page.tsx +++ b/app/(dashboard)/settings/banking/page.tsx @@ -17,6 +17,8 @@ export default function BankingSettingsPage() { const router = useRouter() const { toast } = useToast() const [bankConnectionError, setBankConnectionError] = useState(null) + const [failedBankName, setFailedBankName] = useState(null) + const [isAccessDenied, setIsAccessDenied] = useState(false) const syncInitiatedRef = useRef(false) const abortControllerRef = useRef(null) const unmountedRef = useRef(false) @@ -93,13 +95,18 @@ export default function BankingSettingsPage() { } if (bankError) { - const errorMsg = decodeURIComponent(bankError) + let errorMsg: string + try { errorMsg = decodeURIComponent(bankError) } catch { errorMsg = bankError } + const bankName = searchParams.get('bank_name') + const errorCode = searchParams.get('bank_error_code') toast({ title: 'Anslutning misslyckades', description: errorMsg, variant: 'destructive', }) setBankConnectionError(errorMsg) + if (bankName) setFailedBankName(bankName) + if (errorCode === 'access_denied') setIsAccessDenied(true) router.replace('/settings/banking') } }, [searchParams, router, toast]) @@ -111,12 +118,21 @@ export default function BankingSettingsPage() {

{bankConnectionError}

+ {isAccessDenied && failedBankName && ( +

+ {failedBankName} nekade åtkomst. Om du använder ett privatkonto kan du prova att ansluta med kontotypen "Privatkonto" i bankväljaren nedan. +

+ )}

Du kan också importera transaktioner via bankfil istället.

+ + + + {psuType === 'personal' && ( +

+ Välj Privatkonto om du använder ditt personliga bankkonto för din verksamhet (vanligt för enskild firma). +

+ )} handleConnectBank(bank, psuType)} + onPsuTypeDetected={setPsuType} isConnecting={isConnecting} connectingBankName={connectingBankName} /> diff --git a/extensions/general/enable-banking/index.ts b/extensions/general/enable-banking/index.ts index aea7eba4..32f1f9bd 100644 --- a/extensions/general/enable-banking/index.ts +++ b/extensions/general/enable-banking/index.ts @@ -40,14 +40,27 @@ export const enableBankingExtension: Extension = { handler: async (_request: Request, ctx?: ExtensionContext) => { const log = ctx?.log ?? console try { - const aspsps = await getASPSPs('SE') + // Detect PSU type from company entity_type + let psuType: 'personal' | 'business' = 'business' + if (ctx?.companyId && ctx?.supabase) { + const { data: company } = await ctx.supabase + .from('companies') + .select('entity_type') + .eq('id', ctx.companyId) + .single() + if (company?.entity_type === 'enskild_firma') { + psuType = 'personal' + } + } + + const aspsps = await getASPSPs('SE', psuType) const banks = aspsps.map((aspsp: ASPSP) => ({ name: aspsp.name, country: aspsp.country, logo: aspsp.logo, bic: aspsp.bic, })) - return NextResponse.json({ banks, sandbox: isSandboxMode() }) + return NextResponse.json({ banks, psu_type: psuType, sandbox: isSandboxMode() }) } catch (error) { log.error('Error fetching banks:', error) return NextResponse.json({ @@ -74,7 +87,7 @@ export const enableBankingExtension: Extension = { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - const { aspsp_name, aspsp_country } = await request.json() + const { aspsp_name, aspsp_country, psu_type: explicitPsuType } = await request.json() if (!aspsp_name || !aspsp_country) { return NextResponse.json( @@ -84,9 +97,21 @@ export const enableBankingExtension: Extension = { } try { - // Always use 'business' PSU type — gnubok is accounting software, - // users connect business accounts regardless of entity type (AB or EF) - const psuType = 'business' + // Detect PSU type: explicit override > company entity_type > default 'business' + let psuType: 'personal' | 'business' = 'business' + if (explicitPsuType === 'personal' || explicitPsuType === 'business') { + psuType = explicitPsuType + } else { + const companyId = ctx?.companyId ?? user.id + const { data: company } = await supabase + .from('companies') + .select('entity_type') + .eq('id', companyId) + .single() + if (company?.entity_type === 'enskild_firma') { + psuType = 'personal' + } + } log.info('[enable-banking] Starting bank connection', { user_id: user.id, diff --git a/extensions/general/enable-banking/lib/api-client.ts b/extensions/general/enable-banking/lib/api-client.ts index 02616d26..d6e2c0be 100644 --- a/extensions/general/enable-banking/lib/api-client.ts +++ b/extensions/general/enable-banking/lib/api-client.ts @@ -221,13 +221,13 @@ async function authenticatedFetchWithRetry( /** * Get list of supported banks (ASPSPs) for a country */ -export async function getASPSPs(country: string = 'SE'): Promise { - const psuType = process.env.ENABLE_BANKING_PSU_TYPE || 'business' +export async function getASPSPs(country: string = 'SE', psuType?: 'personal' | 'business'): Promise { + const resolvedPsuType = psuType || process.env.ENABLE_BANKING_PSU_TYPE || 'business' const isSandbox = ENABLE_BANKING_API_URL.includes('tilisy') const params = new URLSearchParams({ country, sandbox: String(isSandbox), - psu_type: psuType, + psu_type: resolvedPsuType, }) const response = await authenticatedFetchWithRetry(`/aspsps?${params.toString()}`) @@ -238,7 +238,7 @@ export async function getASPSPs(country: string = 'SE'): Promise { statusText: response.statusText, body, country, - psuType, + psuType: resolvedPsuType, sandbox: isSandbox, apiUrl: ENABLE_BANKING_API_URL, })