Skip to content

Commit b4eb74b

Browse files
committed
Fix banners
1 parent b6d16ac commit b4eb74b

File tree

8 files changed

+253
-27
lines changed

8 files changed

+253
-27
lines changed

src/controllers/bannerController.ts

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export const getBanners = asyncHandler(async (req: Request, res: Response) => {
1818
page = 1,
1919
limit = 9,
2020
sortBy = 'Priority',
21-
sortOrder = 'desc'
21+
sortOrder = 'asc'
2222
} = req.query;
2323

2424
const query: any = {};
@@ -159,7 +159,7 @@ export const createBanner = asyncHandler(async (req: Request, res: Response) =>
159159
export const updateBanner = asyncHandler(async (req: Request, res: Response) => {
160160
const { id } = req.params;
161161

162-
const banner = await Banner.findById(id).lean();
162+
const banner = await Banner.findById(id);
163163

164164
if (!banner) {
165165
return sendNotFound(res, 'Banner not found');
@@ -184,6 +184,12 @@ export const updateBanner = asyncHandler(async (req: Request, res: Response) =>
184184
// Handle resource project specific logic
185185
const finalBannerData = _handleResourceProjectBannerLogic({ ...validation.data });
186186

187+
// Preserve existing activation date fields and IsActive (not editable in edit form)
188+
finalBannerData.StartDate = banner.StartDate;
189+
finalBannerData.EndDate = banner.EndDate;
190+
finalBannerData.ShowDates = banner.ShowDates;
191+
finalBannerData.IsActive = banner.IsActive;
192+
187193
// Update banner
188194
const updatedBanner = await Banner.findByIdAndUpdate(
189195
id,
@@ -233,20 +239,68 @@ export const deleteBanner = asyncHandler(async (req: Request, res: Response) =>
233239
return sendSuccess(res, {}, 'Banner and associated files deleted successfully');
234240
});
235241

236-
// Toggle banner active status
242+
// @desc Update banner activation status with optional date range
243+
// @route PATCH /api/banners/:id/toggle
244+
// @access Private
237245
export const toggleBannerStatus = asyncHandler(async (req: Request, res: Response) => {
238246
const { id } = req.params;
247+
const { IsActive, StartDate, EndDate } = req.body;
248+
249+
// Get existing banner
250+
const existingBanner = await Banner.findById(id);
251+
if (!existingBanner) {
252+
return sendNotFound(res, 'Banner not found');
253+
}
239254

240-
const banner = await Banner.findById(id);
255+
// Prepare update data
256+
let shouldActivateNow = IsActive !== undefined ? IsActive : !existingBanner.IsActive;
241257

242-
if (!banner) {
243-
return sendNotFound(res, 'Banner not found');
258+
// Check if scheduled start date equals today - if so, activate immediately
259+
if (StartDate !== undefined && StartDate !== null) {
260+
const today = new Date();
261+
today.setHours(0, 0, 0, 0);
262+
263+
const activeFromDate = new Date(StartDate);
264+
activeFromDate.setHours(0, 0, 0, 0);
265+
266+
// If start date is today, activate immediately
267+
if (activeFromDate.getTime() === today.getTime()) {
268+
shouldActivateNow = true;
269+
}
270+
}
271+
272+
const updateData: any = {
273+
IsActive: shouldActivateNow,
274+
DocumentModifiedDate: new Date()
275+
};
276+
277+
// Handle date range for scheduled activation
278+
if (StartDate !== undefined && StartDate !== null) {
279+
updateData.StartDate = new Date(StartDate);
280+
updateData.ShowDates = true;
281+
} else if (updateData.IsActive && !existingBanner.StartDate) {
282+
// If activating immediately without dates, set StartDate to now
283+
updateData.StartDate = new Date();
284+
updateData.ShowDates = true;
285+
}
286+
287+
if (EndDate !== undefined && EndDate !== null) {
288+
updateData.EndDate = new Date(EndDate);
289+
updateData.ShowDates = true;
290+
} else if (!updateData.IsActive && !EndDate) {
291+
// If deactivating without explicit date, set EndDate to now
292+
updateData.EndDate = new Date();
293+
updateData.ShowDates = true;
244294
}
245295

246-
banner.IsActive = !banner.IsActive;
247-
await banner.save();
296+
// Update banner
297+
const updatedBanner = await Banner.findByIdAndUpdate(
298+
id,
299+
updateData,
300+
{ new: true, runValidators: true }
301+
);
248302

249-
return sendSuccess(res, banner, `Banner ${banner.IsActive ? 'activated' : 'deactivated'} successfully`);
303+
return sendSuccess(res, updatedBanner, `Banner ${updatedBanner?.IsActive ? 'activated' : 'deactivated'} successfully`);
250304
});
251305

252306
// Increment download count for resource banners

src/controllers/faqController.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export const getFaqs = asyncHandler(async (req: Request, res: Response) => {
1414
search,
1515
page = 1,
1616
limit = 9,
17-
sortBy = 'DocumentModifiedDate',
18-
sortOrder = 'desc'
17+
sortBy = 'SortPosition',
18+
sortOrder = 'asc'
1919
} = req.query;
2020

2121
const query: any = {};

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import dotenv from 'dotenv';
44
import { startVerificationJob } from './jobs/verificationOrganisationJob.js';
55
import { startDisablingJob } from './jobs/disablingOrganisationJob.js';
66
import { startSwepActivationJob } from './jobs/swepActivationJob.js';
7+
import { startBannerActivationJob } from './jobs/bannerActivationJob.js';
78

89
dotenv.config();
910
connectDB();
@@ -13,6 +14,7 @@ connectDB();
1314
startVerificationJob();
1415
startDisablingJob();
1516
startSwepActivationJob();
17+
startBannerActivationJob();
1618

1719
const PORT:any = process.env.PORT;
1820

src/jobs/bannerActivationJob.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import cron from 'node-cron';
2+
import Banner from '../models/bannerModel.js';
3+
4+
/**
5+
* Background job that runs daily to check banner activation/deactivation dates
6+
* - Checks StartDate to automatically activate banners
7+
* - Checks EndDate to automatically deactivate banners
8+
* - Runs at 00:05 daily to ensure timely activation/deactivation
9+
*/
10+
export function startBannerActivationJob() {
11+
// Run daily at 00:05
12+
cron.schedule('5 0 * * *', async () => {
13+
try {
14+
console.log('Running banner activation/deactivation check job...');
15+
16+
const today = new Date();
17+
today.setUTCHours(0, 0, 0, 0); // Set to start of day UTC for comparison
18+
19+
let activatedCount = 0;
20+
let deactivatedCount = 0;
21+
const errors: string[] = [];
22+
23+
// Find all banners that have scheduling dates
24+
const banners = await Banner.find({
25+
$or: [
26+
{ StartDate: { $exists: true, $ne: null } },
27+
{ EndDate: { $exists: true, $ne: null } }
28+
]
29+
});
30+
31+
console.log(`Found ${banners.length} banner(s) with scheduled dates to check`);
32+
33+
for (const banner of banners) {
34+
try {
35+
const updateData: any = {
36+
DocumentModifiedDate: new Date()
37+
};
38+
let needsUpdate = false;
39+
40+
// Check if banner should be activated today
41+
if (banner.StartDate) {
42+
const startDate = new Date(banner.StartDate);
43+
startDate.setUTCHours(0, 0, 0, 0); // Use UTC for consistent comparison
44+
45+
// If today equals or is after the start date and banner is not active
46+
if (startDate.getTime() === today.getTime() && !banner.IsActive) {
47+
updateData.IsActive = true;
48+
needsUpdate = true;
49+
activatedCount++;
50+
console.log(`Banner activated: ${banner.Title} (ID: ${banner._id})`);
51+
console.log(` - Start date: ${banner.StartDate.toISOString()}`);
52+
}
53+
}
54+
55+
// Check if banner should be deactivated today
56+
if (banner.EndDate) {
57+
const endDate = new Date(banner.EndDate);
58+
endDate.setUTCHours(0, 0, 0, 0); // Use UTC for consistent comparison
59+
endDate.setDate(endDate.getDate() + 1); // Add 1 day to end date
60+
61+
// If today equals or is after the end date and banner is active
62+
if (endDate.getTime() === today.getTime() && banner.IsActive) {
63+
updateData.IsActive = false;
64+
needsUpdate = true;
65+
deactivatedCount++;
66+
console.log(`Banner deactivated: ${banner.Title} (ID: ${banner._id})`);
67+
console.log(` - End date: ${banner.EndDate.toISOString()}`);
68+
}
69+
}
70+
71+
// Update the banner if needed
72+
if (needsUpdate) {
73+
await Banner.findByIdAndUpdate(
74+
banner._id,
75+
{ $set: updateData },
76+
{ runValidators: true }
77+
);
78+
}
79+
} catch (error) {
80+
const errorMsg = `Error updating ${banner.Title}: ${error instanceof Error ? error.message : 'Unknown error'}`;
81+
console.error(errorMsg);
82+
errors.push(errorMsg);
83+
}
84+
}
85+
86+
console.log(`Banner activation check completed:
87+
- Banners checked: ${banners.length}
88+
- Banners activated: ${activatedCount}
89+
- Banners deactivated: ${deactivatedCount}
90+
- Errors: ${errors.length}
91+
`);
92+
93+
if (errors.length > 0) {
94+
console.error('Errors during banner activation check:', errors);
95+
}
96+
} catch (error) {
97+
console.error('Fatal error in banner activation job:', error);
98+
}
99+
});
100+
101+
console.log('Banner activation job scheduled to run daily at 00:05');
102+
}
103+
104+
/**
105+
* One-time check for banners to activate/deactivate (useful for testing)
106+
* Returns statistics about the check
107+
*/
108+
export async function runBannerActivationCheckNow() {
109+
try {
110+
console.log('Running immediate banner activation check...');
111+
112+
const today = new Date();
113+
today.setUTCHours(0, 0, 0, 0); // Use UTC for consistent comparison
114+
115+
const stats = {
116+
total: 0,
117+
needsActivation: 0,
118+
needsDeactivation: 0,
119+
alreadyActive: 0,
120+
alreadyInactive: 0
121+
};
122+
123+
const banners = await Banner.find({
124+
$or: [
125+
{ StartDate: { $exists: true, $ne: null } },
126+
{ EndDate: { $exists: true, $ne: null } }
127+
]
128+
});
129+
130+
stats.total = banners.length;
131+
132+
for (const banner of banners) {
133+
// Check activation date
134+
if (banner.StartDate) {
135+
const startDate = new Date(banner.StartDate);
136+
startDate.setUTCHours(0, 0, 0, 0);
137+
138+
if (startDate.getTime() === today.getTime()) {
139+
if (!banner.IsActive) {
140+
stats.needsActivation++;
141+
} else {
142+
stats.alreadyActive++;
143+
}
144+
}
145+
}
146+
147+
// Check deactivation date
148+
if (banner.EndDate) {
149+
const endDate = new Date(banner.EndDate);
150+
endDate.setUTCHours(0, 0, 0, 0);
151+
endDate.setDate(endDate.getDate() + 1); // Add 1 day
152+
153+
if (endDate.getTime() === today.getTime()) {
154+
if (banner.IsActive) {
155+
stats.needsDeactivation++;
156+
} else {
157+
stats.alreadyInactive++;
158+
}
159+
}
160+
}
161+
}
162+
163+
console.log('Banner activation check stats:', stats);
164+
return stats;
165+
} catch (error) {
166+
console.error('Error in banner activation check:', error);
167+
throw error;
168+
}
169+
}

src/jobs/disablingOrganisationJob.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ import { updateRelatedServices } from '../controllers/organisationController.js'
1010
* - Updates all associated services to unpublished state using transactions
1111
*/
1212
export function startDisablingJob() {
13-
// Run daily at midnight (00:05)
14-
cron.schedule('5 0 * * *', async () => {
13+
// Run daily at midnight (00:10)
14+
cron.schedule('10 0 * * *', async () => {
1515
try {
1616
console.log('Running organisation disabling check job...');
1717

@@ -103,7 +103,7 @@ export function startDisablingJob() {
103103
}
104104
});
105105

106-
console.log('Organisation disabling job scheduled to run daily at midnight (00:05)');
106+
console.log('Organisation disabling job scheduled to run daily at midnight (00:10)');
107107
}
108108

109109
/**

src/routes/bannerRoutes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ router.get('/', bannersByLocationAuth, getBanners);
2121
router.get('/:id', bannersAuth, getBannerById);
2222
router.post('/', bannersAuth, bannersUploadMiddleware, createBanner);
2323
router.put('/:id', bannersAuth, bannersUploadMiddleware, updateBanner);
24-
router.patch('/:id/toggle', bannersAuth, toggleBannerStatus);
24+
router.patch('/:id/toggle-active', bannersAuth, toggleBannerStatus);
2525
router.delete('/:id', bannersAuth, deleteBanner);
2626

2727
export default router;

src/schemas/bannerSchemaCore.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -178,24 +178,25 @@ export const sharedBannerRefinements: RefinementEntry<BannerCore>[] = [
178178
{
179179
refinement: (data: BannerCore) => {
180180
if (data.StartDate && data.EndDate) {
181-
return data.EndDate > data.StartDate;
181+
return data.StartDate <= data.EndDate;
182182
}
183183
return true;
184184
},
185185
message: 'End date must be after start date',
186186
path: ['EndDate']
187187
},
188+
// TODO: Uncomment if need to validate it. But keep in mind that during edit banner we can get validation error because CampaignEndDate can be less than today.
188189
// Campaign end date must be in the future for giving campaigns
189-
{
190-
refinement: (data: BannerCore) => {
191-
if (data.TemplateType === BannerTemplateType.GIVING_CAMPAIGN && data.GivingCampaign?.CampaignEndDate) {
192-
return data.GivingCampaign.CampaignEndDate > new Date();
193-
}
194-
return true;
195-
},
196-
message: 'Campaign end date must be in the future',
197-
path: ['GivingCampaign', 'CampaignEndDate']
198-
},
190+
// {
191+
// refinement: (data: BannerCore) => {
192+
// if (data.TemplateType === BannerTemplateType.GIVING_CAMPAIGN && data.GivingCampaign?.CampaignEndDate) {
193+
// return data.GivingCampaign.CampaignEndDate > new Date();
194+
// }
195+
// return true;
196+
// },
197+
// message: 'Campaign end date must be in the future',
198+
// path: ['GivingCampaign', 'CampaignEndDate']
199+
// },
199200
// Donation target must be positive ONLY for giving campaigns
200201
{
201202
refinement: (data: BannerCore) => {

src/schemas/swepBannerSchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const SwepBannerSchema = z.object({
3535
}).refine((data) => {
3636
// If date range is set, SwepActiveFrom must be before SwepActiveUntil
3737
if (data.SwepActiveFrom && data.SwepActiveUntil) {
38-
return data.SwepActiveFrom < data.SwepActiveUntil;
38+
return data.SwepActiveFrom <= data.SwepActiveUntil;
3939
}
4040

4141
return true;

0 commit comments

Comments
 (0)