Skip to content

Commit d1296f1

Browse files
authored
Fix skipping month (#745)
1 parent 3f37113 commit d1296f1

File tree

2 files changed

+51
-4
lines changed

2 files changed

+51
-4
lines changed

backend/services/recurringTaskService.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,12 +145,29 @@ const calculateMonthlyWeekdayRecurrence = (
145145
};
146146

147147
const calculateMonthlyLastDayRecurrence = (fromDate, interval) => {
148-
const nextDate = new Date(fromDate);
149-
nextDate.setUTCMonth(nextDate.getUTCMonth() + interval);
148+
// Calculate target year and month directly to avoid date overflow
149+
// (e.g., Jan 31 + 1 month via setUTCMonth would overflow to March)
150+
const currentMonth = fromDate.getUTCMonth();
151+
const currentYear = fromDate.getUTCFullYear();
150152

151-
nextDate.setUTCMonth(nextDate.getUTCMonth() + 1, 0);
153+
const totalMonths = currentMonth + interval;
154+
const targetYear = currentYear + Math.floor(totalMonths / 12);
155+
const targetMonth = totalMonths % 12;
152156

153-
return nextDate;
157+
// Get last day of target month by creating date at day 0 of following month
158+
const lastDayOfMonth = new Date(
159+
Date.UTC(
160+
targetYear,
161+
targetMonth + 1, // next month
162+
0, // day 0 = last day of previous month
163+
fromDate.getUTCHours(),
164+
fromDate.getUTCMinutes(),
165+
fromDate.getUTCSeconds(),
166+
fromDate.getUTCMilliseconds()
167+
)
168+
);
169+
170+
return lastDayOfMonth;
154171
};
155172

156173
const getFirstWeekdayOfMonth = (year, month, weekday) => {

backend/tests/integration/recurring-tasks.test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,36 @@ describe('Recurring Tasks', () => {
303303
expect(nextDate.getUTCDate()).toBe(expectedDate.getUTCDate());
304304
});
305305

306+
it('should not skip months for monthly_last_day when starting from 31st', async () => {
307+
// Bug fix: Jan 31 -> should go to Feb 28, not March 31
308+
const jan31 = new Date(Date.UTC(2025, 0, 31, 0, 0, 0, 0));
309+
310+
const taskData = {
311+
name: 'End of Month Task',
312+
recurrence_type: 'monthly_last_day',
313+
recurrence_interval: 1,
314+
due_date: jan31.toISOString().split('T')[0],
315+
};
316+
317+
const response = await agent.post('/api/task').send(taskData);
318+
const task = await Task.findByPk(response.body.id);
319+
320+
// First occurrence: Jan 31 -> Feb 28
321+
const nextDate1 = calculateNextDueDate(task, jan31);
322+
expect(nextDate1.getUTCMonth()).toBe(1); // February
323+
expect(nextDate1.getUTCDate()).toBe(28);
324+
325+
// Second occurrence: Feb 28 -> Mar 31
326+
const nextDate2 = calculateNextDueDate(task, nextDate1);
327+
expect(nextDate2.getUTCMonth()).toBe(2); // March
328+
expect(nextDate2.getUTCDate()).toBe(31);
329+
330+
// Third occurrence: Mar 31 -> Apr 30
331+
const nextDate3 = calculateNextDueDate(task, nextDate2);
332+
expect(nextDate3.getUTCMonth()).toBe(3); // April
333+
expect(nextDate3.getUTCDate()).toBe(30);
334+
});
335+
306336
it('should handle monthly recurrence when day does not exist in target month', async () => {
307337
// Create a task for Jan 31
308338
const jan31 = new Date(Date.UTC(2024, 0, 31, 0, 0, 0, 0));

0 commit comments

Comments
 (0)