Files
accounted/lib/tax/deadline-generator.ts
T
Jakob WennbergandClaude Opus 4.6 091d043c85 feat: UI polish, lint fixes, onboarding redesign, help page expansion, and test improvements
Broad update across dashboard pages, components, extensions, and lib code. Includes ESLint config additions, onboarding flow redesign, settings page refactor, help page content expansion, dead code removal, and test mock fixes. Adds dev docs and public assets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:05:49 +01:00

270 lines
7.6 KiB
TypeScript

/**
* Deadline generator - creates tax deadlines based on company settings
*/
import { SupabaseClient } from '@supabase/supabase-js'
import { createLogger } from '@/lib/logger'
import type { TaxDeadlineType, DeadlineStatus } from '@/types'
const log = createLogger('deadline-generator')
import {
getApplicableDeadlineConfigs,
type CompanySettingsForDeadlines,
type DeadlineInstance,
} from './deadline-config'
import { adjustDeadlineToNextBankingDay } from './swedish-holidays'
/**
* Fields in company_settings that affect tax deadline generation
*/
export const TAX_RELEVANT_FIELDS = [
'entity_type',
'moms_period',
'f_skatt',
'vat_registered',
'pays_salaries',
'fiscal_year_start_month',
] as const
/**
* Check if any tax-relevant fields changed
*/
export function didTaxFieldsChange(
oldSettings: Partial<CompanySettingsForDeadlines>,
newSettings: Partial<CompanySettingsForDeadlines>
): boolean {
for (const field of TAX_RELEVANT_FIELDS) {
if (oldSettings[field] !== newSettings[field]) {
return true
}
}
return false
}
/**
* Format date to YYYY-MM-DD
*/
function formatDateISO(date: Date): string {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
/**
* Generate all tax deadlines for a user based on their company settings
*/
export async function generateTaxDeadlinesForUser(
supabase: SupabaseClient,
userId: string,
settings: CompanySettingsForDeadlines,
years: number[] = []
): Promise<{ created: number; deleted: number }> {
// Default to current and next year if not specified
if (years.length === 0) {
const currentYear = new Date().getFullYear()
years = [currentYear, currentYear + 1]
}
// Get applicable deadline configs based on settings
const applicableConfigs = getApplicableDeadlineConfigs(settings)
// Delete existing system-generated deadlines for these years
const startDate = `${Math.min(...years)}-01-01`
const endDate = `${Math.max(...years)}-12-31`
const { data: deletedData, error: deleteError } = await supabase
.from('deadlines')
.delete()
.eq('user_id', userId)
.eq('source', 'system')
.gte('due_date', startDate)
.lte('due_date', endDate)
.select('id')
if (deleteError) {
log.error('Error deleting existing deadlines:', deleteError)
throw deleteError
}
const deletedCount = deletedData?.length || 0
// Generate new deadlines
const deadlines: Array<{
user_id: string
title: string
due_date: string
deadline_type: 'tax'
priority: 'critical' | 'important' | 'normal'
is_completed: boolean
source: 'system'
status: DeadlineStatus
tax_deadline_type: TaxDeadlineType
tax_period: string
linked_report_type: string | null
linked_report_period: Record<string, unknown> | null
reminder_offsets: number[]
is_auto_generated: boolean
}> = []
for (const config of applicableConfigs) {
for (const year of years) {
const instances = config.generateDates(year, settings)
for (const instance of instances) {
// Create the raw deadline date
const rawDate = new Date(instance.year, instance.month, instance.day)
// Adjust for banking days (skip weekends and holidays)
const adjustedDate = adjustDeadlineToNextBankingDay(rawDate)
const dueDate = formatDateISO(adjustedDate)
// Skip if the deadline is in the past
const today = new Date()
today.setHours(0, 0, 0, 0)
if (adjustedDate < today) {
continue
}
// Determine initial status based on days until deadline
const daysUntil = Math.ceil((adjustedDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))
const status: DeadlineStatus = daysUntil <= 14 ? 'action_needed' : 'upcoming'
// Generate title from template
const title = config.titleTemplate.replace('{periodLabel}', instance.periodLabel)
// Create linked report period data
const linkedReportPeriod = createLinkedReportPeriod(instance, config.type)
deadlines.push({
user_id: userId,
title,
due_date: dueDate,
deadline_type: 'tax',
priority: config.priority,
is_completed: false,
source: 'system',
status,
tax_deadline_type: config.type,
tax_period: instance.period,
linked_report_type: config.linkedReportType,
linked_report_period: linkedReportPeriod,
reminder_offsets: [14, 7, 1, 0],
is_auto_generated: true,
})
}
}
}
// Insert new deadlines
if (deadlines.length > 0) {
const { error: insertError } = await supabase
.from('deadlines')
.insert(deadlines)
if (insertError) {
log.error('Error inserting deadlines:', insertError)
throw insertError
}
}
return {
created: deadlines.length,
deleted: deletedCount,
}
}
/**
* Create linked report period object for navigation
*/
function createLinkedReportPeriod(
instance: DeadlineInstance,
_type: TaxDeadlineType
): Record<string, unknown> | null {
const period = instance.period
// Parse the period string
if (period.includes('-Q')) {
// Quarterly: "2025-Q1"
const [year, quarter] = period.split('-Q')
return { year: parseInt(year), quarter: parseInt(quarter) }
}
if (period.includes('-') && period.length === 7) {
// Monthly: "2025-01"
const [year, month] = period.split('-')
return { year: parseInt(year), month: parseInt(month) }
}
if (period.includes('/')) {
// Fiscal year: "2024/2025"
const [startYear, endYear] = period.split('/')
return { startYear: parseInt(startYear), endYear: parseInt(endYear) }
}
// Annual: "2025"
if (/^\d{4}$/.test(period)) {
return { year: parseInt(period) }
}
return null
}
/**
* Regenerate tax deadlines for a user after settings change
*/
export async function regenerateTaxDeadlinesForUser(
supabase: SupabaseClient,
userId: string,
newSettings: CompanySettingsForDeadlines
): Promise<{ created: number; deleted: number }> {
const currentYear = new Date().getFullYear()
return generateTaxDeadlinesForUser(supabase, userId, newSettings, [currentYear, currentYear + 1])
}
/**
* Generate tax deadlines for the new year (called by annual cron job)
*/
export async function generateNewYearDeadlines(
supabase: SupabaseClient
): Promise<{ usersProcessed: number; totalCreated: number }> {
const newYear = new Date().getFullYear()
// Fetch all users with company settings
const { data: allSettings, error } = await supabase
.from('company_settings')
.select('user_id, entity_type, moms_period, f_skatt, vat_registered, pays_salaries, fiscal_year_start_month')
if (error) {
log.error('Error fetching company settings:', error)
throw error
}
let usersProcessed = 0
let totalCreated = 0
for (const settings of allSettings || []) {
try {
const result = await generateTaxDeadlinesForUser(
supabase,
settings.user_id,
{
entity_type: settings.entity_type,
moms_period: settings.moms_period,
f_skatt: settings.f_skatt,
vat_registered: settings.vat_registered,
pays_salaries: settings.pays_salaries ?? false,
fiscal_year_start_month: settings.fiscal_year_start_month,
},
[newYear, newYear + 1]
)
usersProcessed++
totalCreated += result.created
} catch (err) {
log.error(`Error generating deadlines for user ${settings.user_id}:`, err)
}
}
return { usersProcessed, totalCreated }
}