* feat: implement viewer role permissions for bank transaction imports and connections * feat: add booking_template_library table with 30 system templates Three-level scoping (system/team/company), RLS policies for read/write/delete, and pre-seeded templates for EU reverse charge, tax account, private transfers, salary, representation, year-end, VAT netting, and bank/finance scenarios. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add template library types, helpers, and tests - BookingTemplateLibrary/Line/Category types in types/index.ts - applyTemplate() converts template lines + amount into form lines - Category labels, scope helpers for UI display - 8 unit tests for amount calculation, VAT, rounding, and scope detection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add booking template CRUD, export, and import API routes - GET/POST/DELETE /api/settings/booking-templates (list, create, soft-delete) - PUT /api/settings/booking-templates/[id] (update non-system templates) - GET /api/settings/booking-templates/export (JSON download) - POST /api/settings/booking-templates/import (bulk import from JSON) All routes enforce auth, write permissions, and Zod validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add template picker UI and settings management panel - BookingTemplatePicker: dialog with search, category/entity-type filter, line preview, and amount input — integrated into JournalEntryForm - BookingTemplatesPanel: settings page with grouped templates (system/team/company), create dialog, export/import, soft-delete - Settings templates page now shows both booking and counterparty templates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update supabase/migrations/20260413160000_booking_template_library.sql Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
99 lines
4.2 KiB
TypeScript
99 lines
4.2 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { applyTemplate, getTemplateScope, TEMPLATE_CATEGORY_LABELS } from '../template-library'
|
|
import type { BookingTemplateLibraryLine } from '@/types'
|
|
|
|
describe('applyTemplate', () => {
|
|
it('creates simple two-line debit/credit entries', () => {
|
|
const lines: BookingTemplateLibraryLine[] = [
|
|
{ account: '1630', label: 'Skattekonto', side: 'debit', type: 'business', ratio: 1.0 },
|
|
{ account: '1930', label: 'Företagskonto', side: 'credit', type: 'settlement', ratio: 1.0 },
|
|
]
|
|
const result = applyTemplate(lines, 10000)
|
|
expect(result).toHaveLength(2)
|
|
expect(result[0]).toEqual({
|
|
account_number: '1630',
|
|
debit_amount: '10000.00',
|
|
credit_amount: '',
|
|
line_description: 'Skattekonto',
|
|
})
|
|
expect(result[1]).toEqual({
|
|
account_number: '1930',
|
|
debit_amount: '',
|
|
credit_amount: '10000.00',
|
|
line_description: 'Företagskonto',
|
|
})
|
|
})
|
|
|
|
it('calculates VAT correctly for reverse charge (EU purchase)', () => {
|
|
const lines: BookingTemplateLibraryLine[] = [
|
|
{ account: '4010', label: 'Varuinköp', side: 'debit', type: 'business', ratio: 1.0 },
|
|
{ account: '2614', label: 'Utgående moms', side: 'credit', type: 'vat', vat_rate: 0.25 },
|
|
{ account: '2645', label: 'Ingående moms', side: 'debit', type: 'vat', vat_rate: 0.25 },
|
|
{ account: '1930', label: 'Företagskonto', side: 'credit', type: 'settlement', ratio: 1.0 },
|
|
]
|
|
// Total payment is 10000 SEK (no VAT on the payment itself for reverse charge)
|
|
const result = applyTemplate(lines, 10000)
|
|
expect(result).toHaveLength(4)
|
|
// Business line = 10000 * 1.0
|
|
expect(result[0].debit_amount).toBe('10000.00')
|
|
// VAT = 10000 * 0.25 / (1 + 0.25) = 2000
|
|
expect(result[1].credit_amount).toBe('2000.00')
|
|
expect(result[2].debit_amount).toBe('2000.00')
|
|
// Settlement = 10000
|
|
expect(result[3].credit_amount).toBe('10000.00')
|
|
})
|
|
|
|
it('handles representation with 25% input VAT', () => {
|
|
const lines: BookingTemplateLibraryLine[] = [
|
|
{ account: '6072', label: 'Representation', side: 'debit', type: 'business', ratio: 1.0 },
|
|
{ account: '2641', label: 'Ingående moms', side: 'debit', type: 'vat', vat_rate: 0.25 },
|
|
{ account: '1930', label: 'Företagskonto', side: 'credit', type: 'settlement', ratio: 1.0 },
|
|
]
|
|
// Total paid = 1250 (1000 + 250 VAT)
|
|
const result = applyTemplate(lines, 1250)
|
|
expect(result).toHaveLength(3)
|
|
// Business = 1250 (the representation cost at full ratio)
|
|
expect(result[0].debit_amount).toBe('1250.00')
|
|
// VAT = 1250 * 0.25 / 1.25 = 250
|
|
expect(result[1].debit_amount).toBe('250.00')
|
|
// Settlement = 1250
|
|
expect(result[2].credit_amount).toBe('1250.00')
|
|
})
|
|
|
|
it('rounds monetary values to 2 decimal places', () => {
|
|
const lines: BookingTemplateLibraryLine[] = [
|
|
{ account: '4010', label: 'Varuinköp', side: 'debit', type: 'business', ratio: 1.0 },
|
|
{ account: '2641', label: 'Ingående moms', side: 'debit', type: 'vat', vat_rate: 0.25 },
|
|
{ account: '1930', label: 'Bank', side: 'credit', type: 'settlement', ratio: 1.0 },
|
|
]
|
|
// 333.33 should produce clean rounding
|
|
const result = applyTemplate(lines, 333.33)
|
|
expect(result[0].debit_amount).toBe('333.33')
|
|
// 333.33 * 0.25 / 1.25 = 66.666 → 66.67
|
|
expect(result[1].debit_amount).toBe('66.67')
|
|
expect(result[2].credit_amount).toBe('333.33')
|
|
})
|
|
})
|
|
|
|
describe('getTemplateScope', () => {
|
|
it('identifies system templates', () => {
|
|
expect(getTemplateScope({ is_system: true, team_id: null, company_id: null })).toBe('system')
|
|
})
|
|
|
|
it('identifies team templates', () => {
|
|
expect(getTemplateScope({ is_system: false, team_id: 'team-1', company_id: null })).toBe('team')
|
|
})
|
|
|
|
it('identifies company templates', () => {
|
|
expect(getTemplateScope({ is_system: false, team_id: null, company_id: 'comp-1' })).toBe('company')
|
|
})
|
|
})
|
|
|
|
describe('TEMPLATE_CATEGORY_LABELS', () => {
|
|
it('has labels for all categories', () => {
|
|
expect(Object.keys(TEMPLATE_CATEGORY_LABELS)).toHaveLength(9)
|
|
expect(TEMPLATE_CATEGORY_LABELS.eu_trade).toBe('EU-handel')
|
|
expect(TEMPLATE_CATEGORY_LABELS.tax_account).toBe('Skattekonto')
|
|
})
|
|
})
|