Fix/m sprint fixes (#613)

* fix(dashboard): exclude ignored and already-triaged transactions from stale count

The "Gamla transaktioner" widget counted transactions that had been ignored
or already marked as is_business=true but not yet booked, so users saw a
nag for a row they had already dealt with — and the /transactions inbox
correctly hid it. Align the count with the inbox criterion (is_business
IS NULL, is_ignored = false) so the widget clears when the row leaves
the inbox.

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

* fix(transactions): read entity_type from settings response wrapper

The transactions page read entityRes.entity_type directly, but
/api/settings returns { data: { entity_type, ... } }. The expression
was always undefined, so setEntityType never fired and entityType
stayed at its initial 'enskild_firma'. The template picker's
entity_type filter then dropped every aktiebolag-tagged user template
for AB customers — only entity_type='all' templates made it through.

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

* stale templates
bank sync
journal entry from transaction

* fixed pr comments

* fixed pr comment

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-05-30 01:28:41 +02:00
committed by GitHub
co-authored by Claude Opus 4.7
parent fc7a46c3f2
commit ea1bf01f1e
76 changed files with 5474 additions and 532 deletions
+175 -1
View File
@@ -1,5 +1,12 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { calculateSalary, calculateKarensavdrag, calculateSjuklon, calculateAvgifterRate, calculateVacationAccrual } from '../calculation-engine'
import {
calculateSalary,
calculateKarensavdrag,
calculateSjuklon,
calculateAvgifterRate,
calculateVacationAccrual,
prorateBaseSalaryForPeriod,
} from '../calculation-engine'
import type { PayrollConfig } from '../payroll-config'
import type { TaxTableRate } from '../tax-tables'
@@ -384,6 +391,173 @@ describe('calculateSalary', () => {
})
})
// ============================================================
// Partial-month employment proration
// ============================================================
//
// May 2026 calendar (Mon-Fri only):
// May 1 (Fri), May 4-8 (5), May 11-15 (5), May 18-22 (5), May 25-29 (5)
// → 21 workdays total in May.
// An employee hired May 15 (Fri) works May 15, 18-22, 25-29 → 11 workdays.
// 11 / 21 ≈ 0.5238 → 40 000 SEK × 0.5238 ≈ 20 952,38 SEK.
describe('partial-month employment proration', () => {
it('prorates base salary for an employee hired mid-month', () => {
const result = calculateSalary(
makeBasicInput({
periodStart: '2026-05-01',
periodEnd: '2026-05-31',
employmentStart: '2026-05-15',
employmentEnd: null,
}),
config2026,
emptyTaxRates,
)
// 40 000 × 11 / 21 = 20 952,38 (rounded via engine's r())
expect(result.grossSalary).toBeCloseTo(20952.38, 2)
})
it('prorates base salary for an employee terminated mid-month', () => {
const result = calculateSalary(
makeBasicInput({
periodStart: '2026-05-01',
periodEnd: '2026-05-31',
employmentStart: '2020-01-01',
employmentEnd: '2026-05-15',
}),
config2026,
emptyTaxRates,
)
// May 1, 4-8, 11-15 = 11 workdays → 40 000 × 11 / 21
expect(result.grossSalary).toBeCloseTo(20952.38, 2)
})
it('does not prorate when the employee covers the full period', () => {
const result = calculateSalary(
makeBasicInput({
periodStart: '2026-05-01',
periodEnd: '2026-05-31',
employmentStart: '2024-01-01',
employmentEnd: null,
}),
config2026,
emptyTaxRates,
)
expect(result.grossSalary).toBe(40000)
})
it('returns 0 gross when employment does not overlap the period', () => {
const result = calculateSalary(
makeBasicInput({
periodStart: '2026-05-01',
periodEnd: '2026-05-31',
employmentStart: '2026-06-01',
employmentEnd: null,
}),
config2026,
emptyTaxRates,
)
expect(result.grossSalary).toBe(0)
})
it('combines employment proration with employment_degree', () => {
const result = calculateSalary(
makeBasicInput({
employmentDegree: 50,
periodStart: '2026-05-01',
periodEnd: '2026-05-31',
employmentStart: '2026-05-15',
employmentEnd: null,
}),
config2026,
emptyTaxRates,
)
// 40 000 × 50% × 11 / 21 = 20 000 × 11 / 21 ≈ 10 476,19
expect(result.grossSalary).toBeCloseTo(10476.19, 2)
})
it('skips proration when period bounds are missing (backward compat)', () => {
const result = calculateSalary(
makeBasicInput({
employmentStart: '2026-05-15',
employmentEnd: null,
}),
config2026,
emptyTaxRates,
)
expect(result.grossSalary).toBe(40000)
})
it('subtracts unpaid_leave once on top of proration (no double-counting)', () => {
// 40 000 × 11/21 (mid-month hire) − 2 × 40 000/21 (two unpaid days)
// = 20 952,38 − 3 809,52
// = 17 142,86
const result = calculateSalary(
makeBasicInput({
periodStart: '2026-05-01',
periodEnd: '2026-05-31',
employmentStart: '2026-05-15',
employmentEnd: null,
lineItems: [
{
itemType: 'unpaid_leave',
amount: -3809.52, // = 2 × Math.round((40000/21) * 100) / 100
isTaxable: true,
isAvgiftBasis: true,
isVacationBasis: false,
isGrossDeduction: false,
isNetDeduction: false,
},
],
}),
config2026,
emptyTaxRates,
)
expect(result.grossSalary).toBeCloseTo(17142.86, 2)
})
})
describe('prorateBaseSalaryForPeriod', () => {
it('returns 1 when all dates are missing', () => {
expect(prorateBaseSalaryForPeriod(undefined, undefined, undefined, undefined)).toBe(1)
})
it('returns 1 when employment fully covers the period', () => {
expect(
prorateBaseSalaryForPeriod('2020-01-01', null, '2026-05-01', '2026-05-31'),
).toBe(1)
})
it('returns 11/21 for May 2026 mid-month hire on the 15th', () => {
const ratio = prorateBaseSalaryForPeriod(
'2026-05-15',
null,
'2026-05-01',
'2026-05-31',
)
expect(ratio).toBeCloseTo(11 / 21, 6)
})
it('returns 0 when employment ends before the period starts', () => {
expect(
prorateBaseSalaryForPeriod('2020-01-01', '2026-04-30', '2026-05-01', '2026-05-31'),
).toBe(0)
})
it('returns 0 when employment starts after the period ends', () => {
expect(
prorateBaseSalaryForPeriod('2026-06-01', null, '2026-05-01', '2026-05-31'),
).toBe(0)
})
})
// ============================================================
// Hardening — realistic API flow & cross-rule invariants
// ============================================================