import { err, ok } from "@tailor-platform/erp-kit/core"; import { accountingPeriodLifecycle, type AccountingPeriodStatus, } from "../db/accountingPeriod.lifecycle.generated"; import type { AccountingPeriodPeriodType } from "../generated/enums"; import { FiscalYearNotFoundError, NameRequiredError, InvalidDateRangeError, OverlappingPeriodError, DuplicatePeriodDatesError, InvalidStatusTransitionError, HasAssociatedJournalEntriesError, } from "../lib/errors.generated"; import type { FiscalYear } from "./fiscalYear"; export interface AccountingPeriod { readonly id: string; readonly companyId: string; readonly fiscalYearId: string; readonly name: string; readonly startDate: Date; readonly endDate: Date; readonly periodType: AccountingPeriodPeriodType; readonly status: AccountingPeriodStatus; } export interface CreateAccountingPeriodInput { companyId: string; fiscalYearId: string; name: string; startDate: Date; endDate: Date; periodType: AccountingPeriodPeriodType; } export function validateCreation( input: CreateAccountingPeriodInput, fiscalYear: FiscalYear | null, ) { if (!fiscalYear) { return err(new FiscalYearNotFoundError(input.fiscalYearId)); } if (fiscalYear.companyId !== input.companyId) { return err(new FiscalYearNotFoundError(input.fiscalYearId)); } // Validate name is non-empty if (!input.name || input.name.trim() === "") { return err(new NameRequiredError(String(input.name))); } // Validate start date is strictly before end date const startTime = new Date(input.startDate).getTime(); const endTime = new Date(input.endDate).getTime(); if (startTime >= endTime) { return err(new InvalidDateRangeError(`${String(input.startDate)} - ${String(input.endDate)}`)); } // Validate period falls within fiscal year date range (local-time date-only comparison) const toDateNum = (d: Date | string) => { const dt = new Date(d); return dt.getFullYear() * 10000 + (dt.getMonth() + 1) * 100 + dt.getDate(); }; const periodStartNum = toDateNum(input.startDate); const periodEndNum = toDateNum(input.endDate); const fyStartNum = toDateNum(fiscalYear.startDate); const fyEndNum = toDateNum(fiscalYear.endDate); if (periodStartNum < fyStartNum || periodEndNum > fyEndNum) { return err( new InvalidDateRangeError( `Period dates must fall within fiscal year range: ${String(input.startDate)} - ${String(input.endDate)}`, ), ); } return ok({}); } export function create>( input: CreateAccountingPeriodInput & CF, fiscalYear: FiscalYear | null, existingPeriods: readonly AccountingPeriod[], ) { const valid = validateCreation(input, fiscalYear); if (!valid.ok) return valid; const startTime = new Date(input.startDate).getTime(); const endTime = new Date(input.endDate).getTime(); // Check for duplicate dates (identical start and end, neither is adjustment) for (const existing of existingPeriods) { const existingStart = new Date(existing.startDate).getTime(); const existingEnd = new Date(existing.endDate).getTime(); if (existingStart === startTime && existingEnd === endTime) { if (existing.periodType !== "ADJUSTMENT" && input.periodType !== "ADJUSTMENT") { return err(new DuplicatePeriodDatesError(input.name)); } // Adjustment period sharing dates with a regular period is allowed continue; } } // Check for overlapping periods (excluding adjustment periods that share end dates) for (const existing of existingPeriods) { const existingStart = new Date(existing.startDate).getTime(); const existingEnd = new Date(existing.endDate).getTime(); // Skip overlap check when one is an adjustment period sharing the same end date if ( (input.periodType === "ADJUSTMENT" || existing.periodType === "ADJUSTMENT") && existingEnd === endTime ) { continue; } // Standard overlap check: two ranges overlap if startA < endB and startB < endA if (startTime < existingEnd && existingStart < endTime) { return err(new OverlappingPeriodError(input.name)); } } const { companyId, fiscalYearId, name, startDate, endDate, periodType, ...customFields } = input; return ok({ ...customFields, companyId, fiscalYearId, name, startDate, endDate, periodType, status: "NEVER_OPENED" as const, }); } export type PeriodTransition = | "openPeriod" | "closePeriod" | "reopenPeriod" | "permanentlyClosePeriod"; export function transition(period: AccountingPeriod, operation: PeriodTransition) { const status = accountingPeriodLifecycle.tryTransition(period.status, operation); if (!status) return err(new InvalidStatusTransitionError(period.id)); return ok({ status }); } export function checkDeletable(id: string, hasEntries: boolean) { return hasEntries ? err(new HasAssociatedJournalEntriesError(id)) : ok({}); }