47938520d6
The validator enforced a 6-month minimum on the FIRST fiscal period, citing BFL 3 kap. The law says the opposite: BFL 3 kap 3 par expressly allows a rakenskapsar shorter than 12 months, with no floor, when bokforingsskyldigheten begins (Bolagsverket: the first year may be "hur kort som helst", max 18 months). The floor only applied to isFirstPeriod, exactly the case the law exempts, and blocked an autumn-registered AB from shortening its first year to Dec 31 to file an early arsredovisning. Drop the minimum, keep the 18-month cap and the day-boundary rules, and remove the now-dead Swedish error mapping. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
72 lines
2.8 KiB
TypeScript
72 lines
2.8 KiB
TypeScript
/**
|
|
* Validates fiscal period duration per BFL 3 kap.
|
|
* Maximum 18 months for any fiscal period (first year may be extended).
|
|
* Normal ongoing periods are 12 months. There is NO minimum length: BFL
|
|
* 3 kap 3 § expressly allows a räkenskapsår shorter than 12 months when
|
|
* bokföringsskyldigheten begins or the year is re-laid, with no floor
|
|
* (Bolagsverket: the first year may be "hur kort som helst").
|
|
*/
|
|
|
|
/**
|
|
* Parse a YYYY-MM-DD string into numeric parts without timezone issues.
|
|
* Using new Date(dateStr) is unsafe because it creates UTC midnight,
|
|
* but getDate()/getMonth()/getFullYear() return local-timezone values:
|
|
* shifting the date by -1 day in Western timezones.
|
|
*/
|
|
export function parseDateParts(dateStr: string): { year: number; month: number; day: number } {
|
|
const [year, month, day] = dateStr.split('-').map(Number)
|
|
return { year, month, day }
|
|
}
|
|
|
|
/**
|
|
* Calculate the number of months between two dates (inclusive of partial months).
|
|
* Uses year/month arithmetic only: a mid-month start counts the start month fully,
|
|
* which is conservative for the 18-month cap check.
|
|
*/
|
|
export function monthsBetween(start: string, end: string): number {
|
|
const s = parseDateParts(start)
|
|
const e = parseDateParts(end)
|
|
return (e.year - s.year) * 12 + (e.month - s.month) + 1
|
|
}
|
|
|
|
export interface ValidatePeriodOptions {
|
|
/** Allow any start day (not just 1st of month) for the first fiscal period per BFL 3 kap. */
|
|
isFirstPeriod?: boolean
|
|
}
|
|
|
|
/**
|
|
* Validate a fiscal period's duration and date constraints.
|
|
* Returns null if valid, or an error message string if invalid.
|
|
*/
|
|
export function validatePeriodDuration(start: string, end: string, options?: ValidatePeriodOptions): string | null {
|
|
const startParts = parseDateParts(start)
|
|
const endParts = parseDateParts(end)
|
|
|
|
// end must be after start (YYYY-MM-DD strings are lexicographically orderable)
|
|
if (end <= start) {
|
|
return 'Period end must be after period start'
|
|
}
|
|
|
|
// start must be 1st of month: unless this is the first fiscal period (BFL 3 kap.)
|
|
if (startParts.day !== 1 && !options?.isFirstPeriod) {
|
|
return 'Period start must be the 1st of a month'
|
|
}
|
|
|
|
// end must be last day of month
|
|
// new Date(year, 1-indexed-month, 0) gives the last day of that month
|
|
const lastDayOfEndMonth = new Date(endParts.year, endParts.month, 0).getDate()
|
|
if (endParts.day !== lastDayOfEndMonth) {
|
|
return 'Period end must be the last day of a month'
|
|
}
|
|
|
|
// Max 18 months per BFL 3 kap. No minimum: a first (or re-laid) year may
|
|
// be arbitrarily short, e.g. an autumn-registered AB shortening its first
|
|
// year to end at Dec 31 for an early årsredovisning.
|
|
const months = monthsBetween(start, end)
|
|
if (months > 18) {
|
|
return `Period duration ${months} months exceeds maximum 18 months (BFL 3 kap.)`
|
|
}
|
|
|
|
return null
|
|
}
|