From d78aa9f4d955ddc2bbae53d73a8e4f062cf30b7e Mon Sep 17 00:00:00 2001 From: Sean Templeton Date: Fri, 19 Jan 2024 16:59:18 -0600 Subject: [PATCH] Fix issue with dates not being tracked from start of day --- build.ts | 6 ++ package.json | 2 +- src/AbstractLoanSchedule.ts | 7 +- src/AnnuityLoanSchedule.ts | 6 +- src/BubbleLoanSchedule.ts | 4 +- src/DifferentiatedLoanSchedule.ts | 4 +- test/AbstractLoanSchedule.spec.ts | 129 ++++++++++++++++++++++++------ 7 files changed, 122 insertions(+), 36 deletions(-) create mode 100644 build.ts diff --git a/build.ts b/build.ts new file mode 100644 index 0000000..7cce315 --- /dev/null +++ b/build.ts @@ -0,0 +1,6 @@ +// @ts-ignore +await Bun.build({ + entrypoints: ['./src/index.ts'], + outdir: './dist', + minify: true, +}) diff --git a/package.json b/package.json index 44eb2b3..363c3c0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "lint": "bunx eslint ./src/*.ts --fix", "watch": "bunx tsc -p tsconfig.json --watch", - "build": "bunx rimraf dist && bunx tsc -p tsconfig.json", + "build": "bunx rimraf dist && bun build.ts && bunx tsc --emitDeclarationOnly", "test": "bun test", "coverage": "bun test --coverage" }, diff --git a/src/AbstractLoanSchedule.ts b/src/AbstractLoanSchedule.ts index 36fa3d8..de9e8d9 100644 --- a/src/AbstractLoanSchedule.ts +++ b/src/AbstractLoanSchedule.ts @@ -9,6 +9,7 @@ import { endOfMonth, isSameYear, setDate, + startOfDay, startOfMonth, } from 'date-fns' @@ -33,7 +34,7 @@ export function getInterestByPeriod({ rate, to, from, amount }: InterestParamete } export function getPaymentDate(issueDate: Date, scheduleMonth: number, paymentDay: number): Date { - const paymentDate = addMonths(startOfMonth(issueDate), scheduleMonth) + const paymentDate = addMonths(startOfMonth(startOfDay(issueDate)), scheduleMonth) const paymentEndOfMonth = endOfMonth(paymentDate) return setDate( @@ -126,8 +127,8 @@ export function calculateSchedule

( const minPaymentAmount = Decimal.min(firstPayment.paymentAmount, lastPayment.paymentAmount).toFixed(fixedDecimal) const maxPaymentAmount = Decimal.max(firstPayment.paymentAmount, lastPayment.paymentAmount).toFixed(fixedDecimal) - const dateStart = setDate(initialPayment.paymentDate, 1) - const dateEnd = setDate(lastPayment.paymentDate, 1) + const dateStart = setDate(startOfDay(initialPayment.paymentDate), 1) + const dateEnd = setDate(startOfDay(lastPayment.paymentDate), 1) const term = differenceInMonths(dateEnd, dateStart) diff --git a/src/AnnuityLoanSchedule.ts b/src/AnnuityLoanSchedule.ts index 45dd03d..6913cdf 100644 --- a/src/AnnuityLoanSchedule.ts +++ b/src/AnnuityLoanSchedule.ts @@ -8,7 +8,7 @@ import { getPaymentDateOnWorkingDay, } from './AbstractLoanSchedule' import { Payment, PaymentType, Schedule, ScheduleConfig, ScheduleOptions } from './types' -import { isAfter, isSameDay } from 'date-fns' +import { isAfter, isSameDay, startOfDay } from 'date-fns' import ProdCal from 'prod-cal' export type AnnuityPayment = Payment & { @@ -34,7 +34,7 @@ export function generateAnnuityPayments(parameters: ScheduleConfig, options?: Sc const payments: Array = [ { - ...createInitialPayment(amount, issueDate, rate), + ...createInitialPayment(amount, startOfDay(issueDate), rate), annuityPaymentAmount: 0, }, ] @@ -43,7 +43,7 @@ export function generateAnnuityPayments(parameters: ScheduleConfig, options?: Sc .map(Number.call, Number) .map((termMonth) => ({ paymentDate: getPaymentDateOnWorkingDay( - termMonth === 0 ? new Date(issueDate) : getPaymentDate(issueDate, termMonth, paymentOnDay), + termMonth === 0 ? startOfDay(issueDate) : getPaymentDate(issueDate, termMonth, paymentOnDay), isHoliday, ), paymentType: PaymentType.ER_TYPE_REGULAR, diff --git a/src/BubbleLoanSchedule.ts b/src/BubbleLoanSchedule.ts index cd1e27c..949ef2e 100644 --- a/src/BubbleLoanSchedule.ts +++ b/src/BubbleLoanSchedule.ts @@ -1,7 +1,7 @@ import Decimal from 'decimal.js' import { calculateInterestByPeriod, calculateSchedule, createInitialPayment } from './AbstractLoanSchedule' import { ScheduleOptions, ScheduleConfig, Payment } from './types' -import { addMonths, setDate } from 'date-fns' +import { addMonths, setDate, startOfDay } from 'date-fns' export function generateBubblePayments(parameters: ScheduleConfig, options?: ScheduleOptions) { const fixedDecimal = options?.decimalDigit ?? 2 @@ -38,7 +38,7 @@ export function generateBubblePayments(parameters: ScheduleConfig, options?: Sch }, ] }, - [createInitialPayment(amount, issueDate, rate)] as Payment[], + [createInitialPayment(amount, startOfDay(issueDate), rate)] as Payment[], ) } diff --git a/src/DifferentiatedLoanSchedule.ts b/src/DifferentiatedLoanSchedule.ts index 5cab2ea..326852d 100644 --- a/src/DifferentiatedLoanSchedule.ts +++ b/src/DifferentiatedLoanSchedule.ts @@ -1,7 +1,7 @@ import Decimal from 'decimal.js' import { calculateInterestByPeriod, calculateSchedule, createInitialPayment } from './AbstractLoanSchedule' import { ScheduleOptions, ScheduleConfig, Payment } from './types' -import { addMonths, setDate } from 'date-fns' +import { addMonths, setDate, startOfDay } from 'date-fns' export function generateDifferentiatedPayments(parameters: ScheduleConfig, options?: ScheduleOptions) { const fixedDecimal = options?.decimalDigit ?? 2 @@ -39,7 +39,7 @@ export function generateDifferentiatedPayments(parameters: ScheduleConfig, optio }, ] }, - [createInitialPayment(amount, issueDate, rate)] as Payment[], + [createInitialPayment(amount, startOfDay(issueDate), rate)] as Payment[], ) } diff --git a/test/AbstractLoanSchedule.spec.ts b/test/AbstractLoanSchedule.spec.ts index f063be2..ffa556f 100644 --- a/test/AbstractLoanSchedule.spec.ts +++ b/test/AbstractLoanSchedule.spec.ts @@ -1,40 +1,119 @@ -import { describe, expect, mock, test } from 'bun:test' -import { createHolidayChecker, getPaymentDate, getPaymentDateOnWorkingDay, printSchedule } from '../src' +import { describe, expect, mock, it } from 'bun:test' +import { + calculateSchedule, + createHolidayChecker, + generateAnnuityPayments, + getPaymentDate, + getPaymentDateOnWorkingDay, + printSchedule, + ScheduleConfig, +} from '../src' import ProdCal from 'prod-cal' -describe('AbstractLoan should', () => { - test('print Schedule', () => { - const printFunction = mock(() => false) - - printSchedule( - { - overAllInterest: 0, - amount: 0, - fullAmount: 0, - term: 0, - minPaymentAmount: 0, - maxPaymentAmount: 0, - payments: [], - efficientRate: 0, - }, - printFunction, - ) - expect(printFunction).toHaveBeenCalled() - }) - - test('add month and closest day', () => { +describe('AbstractLoan', () => { + it('should add month and closest day', () => { expect(getPaymentDate(new Date(2013, 4, 1), 33, 31).getTime()).toEqual(new Date(2016, 1, 29).getTime()) }) - test('return payment date as next day after holiday', () => { + it('should return payment date as next day after holiday', () => { expect( getPaymentDateOnWorkingDay(new Date(2015, 4, 1), createHolidayChecker(new ProdCal('ru'))).getTime(), ).toEqual(new Date(2015, 4, 5).getTime()) }) - test('return payment date as closet day before holiday if holiday lasts to the end of the month', () => { + it('should return payment date as closet day before holiday if holiday lasts to the end of the month', () => { expect( getPaymentDateOnWorkingDay(new Date(2015, 4, 31), createHolidayChecker(new ProdCal('ru'))).getTime(), ).toEqual(new Date(2015, 4, 29).getTime()) }) + + describe('printSchedule', () => { + it('should print Schedule', () => { + const printFunction = mock(() => false) + + printSchedule( + { + overAllInterest: 0, + amount: 0, + fullAmount: 0, + term: 0, + minPaymentAmount: 0, + maxPaymentAmount: 0, + payments: [], + efficientRate: 0, + }, + printFunction, + ) + expect(printFunction).toHaveBeenCalled() + }) + }) + + describe('calculateSchedule', () => { + it('should require at least 2 payment records', () => { + expect(() => + calculateSchedule( + { + amount: 500000, + rate: 11.5, + term: 12, + paymentOnDay: 25, + issueDate: new Date(2018, 9, 25), + }, + [], + ), + ).toThrow() + }) + + describe('should return the same term length specified', () => { + it('when no payment amount is specified', () => { + const config: ScheduleConfig = { + amount: 26000, + rate: 18, + term: 60, + paymentOnDay: 25, + issueDate: new Date(2018, 9, 25), + } + const schedule = calculateSchedule(config, generateAnnuityPayments(config)) + expect(schedule.term).toEqual(60) + }) + it('when no payment amount is specified and date has time', () => { + const config: ScheduleConfig = { + amount: 26000, + rate: 18, + term: 60, + paymentOnDay: 25, + issueDate: new Date(2018, 9, 25, 6, 0, 0), + } + const schedule = calculateSchedule(config, generateAnnuityPayments(config)) + expect(schedule.term).toEqual(60) + }) + + it('when payment amount is specified', () => { + const config: ScheduleConfig = { + amount: 26000, + rate: 18, + term: 60, + paymentOnDay: 22, + paymentAmount: 660.23, + issueDate: new Date(2024, 0, 22), + earlyRepayment: [], + } + const schedule = calculateSchedule(config, generateAnnuityPayments(config)) + expect(schedule.term).toEqual(60) + }) + it('when payment amount is specified and date has time', () => { + const config: ScheduleConfig = { + amount: 26000, + rate: 18, + term: 60, + paymentOnDay: 22, + paymentAmount: 660.23, + issueDate: new Date(2024, 0, 22, 6, 0, 0), + earlyRepayment: [], + } + const schedule = calculateSchedule(config, generateAnnuityPayments(config)) + expect(schedule.term).toEqual(60) + }) + }) + }) })