feat: Retry failed messages within 24h (#12436)

Fixes [CW-5540](https://linear.app/chatwoot/issue/CW-5540/feat-allow-retry-a-failed-message-sendout),
https://github.com/chatwoot/chatwoot/issues/12333

## Type of change

- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?

### Screenshot
<img width="196" height="113" alt="image"
src="https://github.com/user-attachments/assets/8b4a1aa9-c75c-4169-b4a2-e89148647e91"
/>



## Checklist:

- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Sivin Varghese
2025-09-16 21:00:15 +05:30
committed by GitHub
parent b0cf1e0f12
commit d45527b851
6 changed files with 200 additions and 2 deletions

View File

@@ -4,6 +4,8 @@ import {
dynamicTime,
dateFormat,
shortTimestamp,
getDayDifferenceFromNow,
hasOneDayPassed,
} from 'shared/helpers/timeHelper';
beforeEach(() => {
@@ -90,3 +92,148 @@ describe('#shortTimestamp', () => {
expect(shortTimestamp('4 years ago', true)).toEqual('4y ago');
});
});
describe('#getDayDifferenceFromNow', () => {
it('returns 0 for timestamps from today', () => {
// Mock current date: May 5, 2023
const now = new Date(Date.UTC(2023, 4, 5, 12, 0, 0)); // 12:00 PM
const todayTimestamp = Math.floor(now.getTime() / 1000); // Same day
expect(getDayDifferenceFromNow(now, todayTimestamp)).toEqual(0);
});
it('returns 2 for timestamps from 2 days ago', () => {
const now = new Date(Date.UTC(2023, 4, 5, 12, 0, 0)); // May 5, 2023
const twoDaysAgoTimestamp = Math.floor(
new Date(Date.UTC(2023, 4, 3, 10, 0, 0)).getTime() / 1000
); // May 3, 2023
expect(getDayDifferenceFromNow(now, twoDaysAgoTimestamp)).toEqual(2);
});
it('returns 7 for timestamps from a week ago', () => {
const now = new Date(Date.UTC(2023, 4, 5, 12, 0, 0)); // May 5, 2023
const weekAgoTimestamp = Math.floor(
new Date(Date.UTC(2023, 3, 28, 8, 0, 0)).getTime() / 1000
); // April 28, 2023
expect(getDayDifferenceFromNow(now, weekAgoTimestamp)).toEqual(7);
});
it('returns 30 for timestamps from a month ago', () => {
const now = new Date(Date.UTC(2023, 4, 5, 12, 0, 0)); // May 5, 2023
const monthAgoTimestamp = Math.floor(
new Date(Date.UTC(2023, 3, 5, 12, 0, 0)).getTime() / 1000
); // April 5, 2023
expect(getDayDifferenceFromNow(now, monthAgoTimestamp)).toEqual(30);
});
it('handles edge case with different times on same day', () => {
const now = new Date(Date.UTC(2023, 4, 5, 23, 59, 59)); // May 5, 2023 11:59:59 PM
const morningTimestamp = Math.floor(
new Date(Date.UTC(2023, 4, 5, 0, 0, 1)).getTime() / 1000
); // May 5, 2023 12:00:01 AM
expect(getDayDifferenceFromNow(now, morningTimestamp)).toEqual(0);
});
it('handles cross-month boundaries correctly', () => {
const now = new Date(Date.UTC(2023, 4, 1, 12, 0, 0)); // May 1, 2023
const lastMonthTimestamp = Math.floor(
new Date(Date.UTC(2023, 3, 30, 12, 0, 0)).getTime() / 1000
); // April 30, 2023
expect(getDayDifferenceFromNow(now, lastMonthTimestamp)).toEqual(1);
});
it('handles cross-year boundaries correctly', () => {
const now = new Date(Date.UTC(2023, 0, 2, 12, 0, 0)); // January 2, 2023
const lastYearTimestamp = Math.floor(
new Date(Date.UTC(2022, 11, 31, 12, 0, 0)).getTime() / 1000
); // December 31, 2022
expect(getDayDifferenceFromNow(now, lastYearTimestamp)).toEqual(2);
});
});
describe('#hasOneDayPassed', () => {
beforeEach(() => {
// Mock current date: May 5, 2023, 12:00 PM UTC (1683288000)
const mockDate = new Date(1683288000 * 1000);
vi.setSystemTime(mockDate);
});
it('returns false for timestamps from today', () => {
// Same day, different time - May 5, 2023 8:00 AM UTC
const todayTimestamp = 1683273600;
expect(hasOneDayPassed(todayTimestamp)).toBe(false);
});
it('returns false for timestamps from yesterday (less than 24 hours)', () => {
// Yesterday but less than 24 hours ago - May 4, 2023 6:00 PM UTC (18 hours ago)
const yesterdayTimestamp = 1683230400;
expect(hasOneDayPassed(yesterdayTimestamp)).toBe(false);
});
it('returns true for timestamps from exactly 1 day ago', () => {
// Exactly 24 hours ago - May 4, 2023 12:00 PM UTC
const oneDayAgoTimestamp = 1683201600;
expect(hasOneDayPassed(oneDayAgoTimestamp)).toBe(true);
});
it('returns true for timestamps from more than 1 day ago', () => {
// 2 days ago - May 3, 2023 10:00 AM UTC
const twoDaysAgoTimestamp = 1683108000;
expect(hasOneDayPassed(twoDaysAgoTimestamp)).toBe(true);
});
it('returns true for timestamps from a week ago', () => {
// 7 days ago - April 28, 2023 8:00 AM UTC
const weekAgoTimestamp = 1682668800;
expect(hasOneDayPassed(weekAgoTimestamp)).toBe(true);
});
it('returns true for null timestamp (defensive check)', () => {
expect(hasOneDayPassed(null)).toBe(true);
});
it('returns true for undefined timestamp (defensive check)', () => {
expect(hasOneDayPassed(undefined)).toBe(true);
});
it('returns true for zero timestamp (defensive check)', () => {
expect(hasOneDayPassed(0)).toBe(true);
});
it('returns true for empty string timestamp (defensive check)', () => {
expect(hasOneDayPassed('')).toBe(true);
});
it('handles cross-month boundaries correctly', () => {
// Set current time to May 1, 2023 12:00 PM UTC (1682942400)
const mayFirst = new Date(1682942400 * 1000);
vi.setSystemTime(mayFirst);
// April 29, 2023 12:00 PM UTC (1682769600) - 2 days ago, crossing month boundary
const crossMonthTimestamp = 1682769600;
expect(hasOneDayPassed(crossMonthTimestamp)).toBe(true);
});
it('handles cross-year boundaries correctly', () => {
// Set current time to January 2, 2023 12:00 PM UTC (1672660800)
const newYear = new Date(1672660800 * 1000);
vi.setSystemTime(newYear);
// December 30, 2022 12:00 PM UTC (1672401600) - 3 days ago, crossing year boundary
const crossYearTimestamp = 1672401600;
expect(hasOneDayPassed(crossYearTimestamp)).toBe(true);
});
});