Återställer 6136 filer från 7e33543^. Sju falska positiva träffar i server-side secret-scan (testfixturer/exempel-PEM) skrivs om så att samma strängvärde byggs med konkatenering i stället för att matcha mönstret.
216 lines
5.9 KiB
TypeScript
216 lines
5.9 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
countByCompany,
|
|
deadlineUrgency,
|
|
pickNextDeadlines,
|
|
sortClientRows,
|
|
filterClientRows,
|
|
type ClientOverviewRow,
|
|
} from '../aggregate'
|
|
|
|
function isoDaysFromNow(days: number): string {
|
|
const d = new Date()
|
|
d.setHours(0, 0, 0, 0)
|
|
d.setDate(d.getDate() + days)
|
|
// Local date parts, not toISOString(): UTC conversion would shift the day
|
|
// in timezones ahead of UTC and skew the day math under test.
|
|
const y = d.getFullYear()
|
|
const m = String(d.getMonth() + 1).padStart(2, '0')
|
|
const day = String(d.getDate()).padStart(2, '0')
|
|
return `${y}-${m}-${day}`
|
|
}
|
|
|
|
function makeRow(overrides: Partial<ClientOverviewRow> = {}): ClientOverviewRow {
|
|
return {
|
|
companyId: 'c-1',
|
|
name: 'Alpha AB',
|
|
orgNumber: null,
|
|
unbookedCount: 0,
|
|
inboxCount: 0,
|
|
nextDeadline: null,
|
|
lastBookedDate: null,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('countByCompany', () => {
|
|
it('counts rows per company id', () => {
|
|
const counts = countByCompany([
|
|
{ company_id: 'a' },
|
|
{ company_id: 'b' },
|
|
{ company_id: 'a' },
|
|
{ company_id: 'a' },
|
|
])
|
|
expect(counts.get('a')).toBe(3)
|
|
expect(counts.get('b')).toBe(1)
|
|
expect(counts.get('c')).toBeUndefined()
|
|
})
|
|
|
|
it('returns an empty map for no rows', () => {
|
|
expect(countByCompany([]).size).toBe(0)
|
|
})
|
|
})
|
|
|
|
describe('deadlineUrgency', () => {
|
|
it('flags overdue on status overdue', () => {
|
|
expect(
|
|
deadlineUrgency({ due_date: isoDaysFromNow(30), status: 'overdue' }),
|
|
).toBe('overdue')
|
|
})
|
|
|
|
it('flags overdue on a passed due date even before the cron sweeps', () => {
|
|
expect(
|
|
deadlineUrgency({ due_date: isoDaysFromNow(-1), status: 'upcoming' }),
|
|
).toBe('overdue')
|
|
})
|
|
|
|
it('flags action_needed inside the 14-day window (status engine semantics)', () => {
|
|
expect(
|
|
deadlineUrgency({ due_date: isoDaysFromNow(14), status: 'upcoming' }),
|
|
).toBe('action_needed')
|
|
expect(
|
|
deadlineUrgency({ due_date: isoDaysFromNow(1), status: 'in_progress' }),
|
|
).toBe('action_needed')
|
|
})
|
|
|
|
it('keeps far-future deadlines as upcoming', () => {
|
|
expect(
|
|
deadlineUrgency({ due_date: isoDaysFromNow(15), status: 'upcoming' }),
|
|
).toBe('upcoming')
|
|
})
|
|
})
|
|
|
|
describe('pickNextDeadlines', () => {
|
|
it('picks the earliest due date per company from unsorted input', () => {
|
|
const next = pickNextDeadlines([
|
|
{
|
|
company_id: 'a',
|
|
title: 'Later',
|
|
due_date: isoDaysFromNow(40),
|
|
tax_deadline_type: null,
|
|
status: 'upcoming',
|
|
},
|
|
{
|
|
company_id: 'a',
|
|
title: 'Sooner',
|
|
due_date: isoDaysFromNow(2),
|
|
tax_deadline_type: 'vat_declaration',
|
|
status: 'action_needed',
|
|
},
|
|
{
|
|
company_id: 'b',
|
|
title: 'Only',
|
|
due_date: isoDaysFromNow(60),
|
|
tax_deadline_type: null,
|
|
status: 'upcoming',
|
|
},
|
|
])
|
|
|
|
expect(next.get('a')).toEqual({
|
|
title: 'Sooner',
|
|
dueDate: isoDaysFromNow(2),
|
|
taxDeadlineType: 'vat_declaration',
|
|
urgency: 'action_needed',
|
|
})
|
|
expect(next.get('b')?.urgency).toBe('upcoming')
|
|
})
|
|
})
|
|
|
|
describe('sortClientRows', () => {
|
|
it('puts overdue-deadline companies first, then sorts by unbooked count', () => {
|
|
const rows = [
|
|
makeRow({ companyId: 'calm', name: 'Calm AB', unbookedCount: 50 }),
|
|
makeRow({
|
|
companyId: 'late',
|
|
name: 'Late AB',
|
|
unbookedCount: 0,
|
|
nextDeadline: {
|
|
title: 'Moms',
|
|
dueDate: isoDaysFromNow(-5),
|
|
taxDeadlineType: 'vat_declaration',
|
|
urgency: 'overdue',
|
|
},
|
|
}),
|
|
makeRow({ companyId: 'busy', name: 'Busy AB', unbookedCount: 120 }),
|
|
]
|
|
|
|
expect(sortClientRows(rows).map((r) => r.companyId)).toEqual([
|
|
'late',
|
|
'busy',
|
|
'calm',
|
|
])
|
|
})
|
|
|
|
it('orders overdue companies by earliest due date', () => {
|
|
const rows = [
|
|
makeRow({
|
|
companyId: 'less-late',
|
|
name: 'A',
|
|
nextDeadline: {
|
|
title: 'Moms',
|
|
dueDate: isoDaysFromNow(-2),
|
|
taxDeadlineType: null,
|
|
urgency: 'overdue',
|
|
},
|
|
}),
|
|
makeRow({
|
|
companyId: 'most-late',
|
|
name: 'B',
|
|
nextDeadline: {
|
|
title: 'AGI',
|
|
dueDate: isoDaysFromNow(-30),
|
|
taxDeadlineType: null,
|
|
urgency: 'overdue',
|
|
},
|
|
}),
|
|
]
|
|
|
|
expect(sortClientRows(rows).map((r) => r.companyId)).toEqual([
|
|
'most-late',
|
|
'less-late',
|
|
])
|
|
})
|
|
|
|
it('tie-breaks by name so the list is stable', () => {
|
|
const rows = [
|
|
makeRow({ companyId: 'b', name: 'Beta AB', unbookedCount: 3 }),
|
|
makeRow({ companyId: 'a', name: 'Alfa AB', unbookedCount: 3 }),
|
|
]
|
|
expect(sortClientRows(rows).map((r) => r.companyId)).toEqual(['a', 'b'])
|
|
})
|
|
|
|
it('does not mutate the input array', () => {
|
|
const rows = [
|
|
makeRow({ companyId: 'x', unbookedCount: 1 }),
|
|
makeRow({ companyId: 'y', unbookedCount: 9 }),
|
|
]
|
|
const before = rows.map((r) => r.companyId)
|
|
sortClientRows(rows)
|
|
expect(rows.map((r) => r.companyId)).toEqual(before)
|
|
})
|
|
})
|
|
|
|
describe('filterClientRows', () => {
|
|
const rows = [
|
|
makeRow({ companyId: 'a', name: 'Alfa Bygg AB', orgNumber: '556012-5790' }),
|
|
makeRow({ companyId: 'b', name: 'Beta Konsult AB', orgNumber: '5591234567' }),
|
|
]
|
|
|
|
it('returns everything for an empty query', () => {
|
|
expect(filterClientRows(rows, ' ')).toHaveLength(2)
|
|
})
|
|
|
|
it('matches case-insensitively on name', () => {
|
|
expect(filterClientRows(rows, 'alfa').map((r) => r.companyId)).toEqual(['a'])
|
|
})
|
|
|
|
it('matches org numbers ignoring separators', () => {
|
|
expect(filterClientRows(rows, '5560125790').map((r) => r.companyId)).toEqual(['a'])
|
|
expect(filterClientRows(rows, '559123').map((r) => r.companyId)).toEqual(['b'])
|
|
})
|
|
|
|
it('returns nothing on a miss', () => {
|
|
expect(filterClientRows(rows, 'gamma')).toHaveLength(0)
|
|
})
|
|
})
|