UNPKG

@accounter/server

Version:
199 lines (173 loc) • 7.29 kB
import { addDays, differenceInDays, getDaysInYear, getYear, isAfter, isBefore, isSameYear, startOfYear, } from 'date-fns'; import { GraphQLError } from 'graphql'; import type { Injector } from 'graphql-modules'; import { DepreciationCategoriesProvider } from '../../depreciation/providers/depreciation-categories.provider.js'; import { DepreciationProvider } from '../../depreciation/providers/depreciation.provider.js'; /** * Calculates depreciation amounts for accounting purposes * @param depreciationRate - Annual depreciation rate (e.g., 0.07 for 7%) * @param value - Initial value of the asset in ILS * @param activationDate - Date when the asset depreciation began * @param calculationYear - Year for which to calculate depreciation (as of December 31st) * @param depreciationEndDate - Optional date when depreciation ends (e.g., asset sold) * @returns Object containing yearly depreciation amount and past depreciation amount */ export function calculateDepreciation( depreciationRate: number, value: number, activationDate: Date, calculationYear: number, depreciationEndDate?: Date, ): { yearlyDepreciationAmount: number; yearlyDepreciationRate: number; pastDepreciationAmount: number; } { const valueAmount = Math.round(value); // Normalize depreciation rate (convert percentage to decimal) const normalizedRate = depreciationRate / 100; // Calculate end date if not provided if (!depreciationEndDate) { const yearsToFullDepreciation = 1 / normalizedRate; const activationYear = getYear(activationDate); const fullDepreciationYear = activationYear + Math.ceil(yearsToFullDepreciation); // Calculate proportional days for the fractional year const fullYears = Math.floor(yearsToFullDepreciation); const fractionalYear = yearsToFullDepreciation - fullYears; if (fractionalYear > 0) { const daysInFinalYear = getDaysInYear(new Date(fullDepreciationYear, 0, 1)); const daysToAdd = Math.ceil(fractionalYear * daysInFinalYear); depreciationEndDate = addDays(new Date(fullDepreciationYear, 0, 1), daysToAdd - 1); } else { depreciationEndDate = new Date(fullDepreciationYear, 0, 0); // Last day of previous year } } // Create calculation date as December 31st of the calculation year const calculationDate = new Date(calculationYear, 11, 31); const startOfCalculationYear = startOfYear(calculationDate); // If calculation date is before activation date, no depreciation applies if (isAfter(activationDate, calculationDate)) { return { yearlyDepreciationAmount: 0, yearlyDepreciationRate: 0, pastDepreciationAmount: 0 }; } // If depreciation ended before the calculation year, no yearly depreciation if (isBefore(depreciationEndDate, startOfCalculationYear)) { // Calculate total depreciation up to the end date const totalYearsActive = differenceInDays(depreciationEndDate, activationDate) / 365.25; const totalDepreciation = Math.min( valueAmount, Math.round(valueAmount * normalizedRate * totalYearsActive), ); return { yearlyDepreciationAmount: 0, yearlyDepreciationRate: 0, pastDepreciationAmount: totalDepreciation, }; } // Calculate annual depreciation amount const annualDepreciationAmount = Math.round(valueAmount * normalizedRate); // Handle different cases for calculation const activationYear = getYear(activationDate); const effectiveEndDate = isBefore(depreciationEndDate, calculationDate) ? depreciationEndDate : calculationDate; // Calculate past depreciation (up to the start of calculation year) let pastDepreciationAmount = 0; if (activationYear < calculationYear) { // Use recursion to calculate past depreciation for previous years const prevYearDepreciation = calculateDepreciation( depreciationRate, valueAmount, activationDate, calculationYear - 1, depreciationEndDate, ); pastDepreciationAmount += prevYearDepreciation.yearlyDepreciationAmount + prevYearDepreciation.pastDepreciationAmount; } // Cap past depreciation at asset value pastDepreciationAmount = Math.min(pastDepreciationAmount, valueAmount); // Calculate depreciation for the calculation year let yearlyDepreciationAmount: number; if (activationYear === calculationYear) { // Asset activated during calculation year const daysInYear = getDaysInYear(calculationDate); const daysActive = differenceInDays(effectiveEndDate, activationDate) + 1; yearlyDepreciationAmount = Math.round(annualDepreciationAmount * (daysActive / daysInYear)); } else if (isSameYear(depreciationEndDate, calculationDate)) { // Use remaining value for the year if depreciation ends this year yearlyDepreciationAmount = Math.max(0, valueAmount - pastDepreciationAmount); } else { // Asset active for the full calculation year yearlyDepreciationAmount = annualDepreciationAmount; } // Ensure we don't depreciate more than the remaining value const remainingValue = Math.max(0, valueAmount - pastDepreciationAmount); yearlyDepreciationAmount = Math.min(yearlyDepreciationAmount, remainingValue); return { yearlyDepreciationAmount, yearlyDepreciationRate: depreciationRate * (yearlyDepreciationAmount / annualDepreciationAmount), pastDepreciationAmount, }; } export async function calculateDepreciationAmount(injector: Injector, year: number) { const yearBeginning = new Date(year, 0, 1); const yearEnd = new Date(year, 11, 31); const depreciationRecords = await injector .get(DepreciationProvider) .getDepreciationRecordsByDates({ fromDate: yearBeginning, toDate: yearEnd, }); let rndDepreciationYearlyAmount = 0; let gnmDepreciationYearlyAmount = 0; let marketingDepreciationYearlyAmount = 0; await Promise.all( depreciationRecords.map(async record => { const depreciationCategory = await injector .get(DepreciationCategoriesProvider) .getDepreciationCategoriesByIdLoader.load(record.category); if (!depreciationCategory) { console.error('No depreciation category for depreciation record', record); return; } const { yearlyDepreciationAmount } = calculateDepreciation( Number(depreciationCategory.percentage), Number(record.amount), record.activation_date, year, record.expiration_date ?? undefined, ); switch (record.type) { case 'RESEARCH_AND_DEVELOPMENT': rndDepreciationYearlyAmount += yearlyDepreciationAmount; break; case 'GENERAL_AND_MANAGEMENT': gnmDepreciationYearlyAmount += yearlyDepreciationAmount; break; case 'MARKETING': marketingDepreciationYearlyAmount += yearlyDepreciationAmount; break; default: throw new GraphQLError(`Unknown depreciation record type ${record.type}`); } }), ); const totalDepreciationYearlyAmount = rndDepreciationYearlyAmount + gnmDepreciationYearlyAmount + marketingDepreciationYearlyAmount; return { rndDepreciationYearlyAmount, gnmDepreciationYearlyAmount, marketingDepreciationYearlyAmount, totalDepreciationYearlyAmount, }; }