Files
accounted/lib/bookkeeping/__tests__/template-library.test.ts
T
bf36ebfd88 feat: booking template library with system templates and cross-company sharing (#235)
* 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>
2026-04-14 09:42:14 +02:00

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')
})
})