Add global settings to make event_date and expiration optional when creating galleries. This supports non-event use cases like portraits, corporate shoots, etc. New features: - Settings toggles in Settings → Event Creation tab - "Require event date" checkbox with warning about random URL identifiers - "Require expiration date" checkbox with warning about manual archiving - Galleries without date use random hex suffix in slug (e.g. portrait-smith-a1b2c3) - Galleries without expiration never expire (stay active until archived) Backend changes: - New migration for settings and nullable columns - Conditional validation based on settings - Updated slug generation with random suffix fallback - Updated expiration checker to skip null expires_at - Updated gallery access control for null expiration Frontend changes: - New checkboxes in EventsTab with warnings - Conditional event date field (shows optional label) - No Expiration message when expiration disabled - Updated types for nullable event_date and expires_at Closes #118
115 lines
3.9 KiB
JavaScript
115 lines
3.9 KiB
JavaScript
const cron = require('node-cron');
|
|
const { db } = require('../database/db');
|
|
const { archiveEvent } = require('./archiveService');
|
|
const { queueEmail } = require('./emailProcessor');
|
|
const logger = require('../utils/logger');
|
|
const { formatDate } = require('../utils/dateFormatter');
|
|
const { formatBoolean } = require('../utils/dbCompat');
|
|
|
|
function startExpirationChecker() {
|
|
// Check every hour for expired events and warnings
|
|
cron.schedule('0 * * * *', async () => {
|
|
await checkExpirations();
|
|
});
|
|
|
|
logger.info('Expiration checker started');
|
|
}
|
|
|
|
async function checkExpirations() {
|
|
try {
|
|
const now = new Date();
|
|
const warningDate = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000); // 7 days from now
|
|
|
|
// Check for events needing warning emails
|
|
// Skip events with null expires_at (they never expire)
|
|
const eventsNeedingWarning = await db('events')
|
|
.where('is_active', formatBoolean(true))
|
|
.where('is_archived', formatBoolean(false))
|
|
.whereNotNull('expires_at')
|
|
.where('expires_at', '<=', warningDate)
|
|
.where('expires_at', '>', now);
|
|
|
|
for (const event of eventsNeedingWarning) {
|
|
// Check if warning email already sent
|
|
const existingWarning = await db('email_queue')
|
|
.where('event_id', event.id)
|
|
.where('email_type', 'expiration_warning')
|
|
.first();
|
|
|
|
if (!existingWarning) {
|
|
await queueExpirationWarning(event);
|
|
}
|
|
}
|
|
|
|
// Check for expired events
|
|
// Skip events with null expires_at (they never expire)
|
|
const expiredEvents = await db('events')
|
|
.where('is_active', formatBoolean(true))
|
|
.where('is_archived', formatBoolean(false))
|
|
.whereNotNull('expires_at')
|
|
.where('expires_at', '<=', now);
|
|
|
|
for (const event of expiredEvents) {
|
|
await handleExpiredEvent(event);
|
|
}
|
|
|
|
} catch (error) {
|
|
logger.error('Error checking expirations:', error);
|
|
}
|
|
}
|
|
|
|
async function queueExpirationWarning(event) {
|
|
const daysRemaining = Math.ceil((new Date(event.expires_at) - new Date()) / (1000 * 60 * 60 * 24));
|
|
|
|
// Determine language based on email domain
|
|
const recipientEmail = event.customer_email || event.host_email;
|
|
const recipientName = event.customer_name || event.host_name || (recipientEmail ? recipientEmail.split('@')[0] : null);
|
|
const emailLang = recipientEmail && recipientEmail.endsWith('.de') ? 'de' : 'en';
|
|
|
|
// Queue email to customer
|
|
await queueEmail(event.id, recipientEmail, 'expiration_warning', {
|
|
customer_name: recipientName,
|
|
customer_email: recipientEmail,
|
|
host_name: recipientName,
|
|
event_name: event.event_name,
|
|
days_remaining: daysRemaining.toString(),
|
|
expiration_date: await formatDate(event.expires_at, emailLang),
|
|
gallery_link: event.share_link
|
|
});
|
|
|
|
logger.info(`Queued expiration warning for event ${event.slug}`);
|
|
}
|
|
|
|
async function handleExpiredEvent(event) {
|
|
try {
|
|
// Mark as inactive
|
|
await db('events').where('id', event.id).update({ is_active: formatBoolean(false) });
|
|
|
|
// Queue expiration emails
|
|
const recipientEmail = event.customer_email || event.host_email;
|
|
const recipientName = event.customer_name || event.host_name || (recipientEmail ? recipientEmail.split('@')[0] : null);
|
|
|
|
await queueEmail(event.id, recipientEmail, 'gallery_expired', {
|
|
event_name: event.event_name,
|
|
admin_email: event.admin_email,
|
|
customer_name: recipientName,
|
|
customer_email: recipientEmail
|
|
});
|
|
|
|
// Also notify admin
|
|
await queueEmail(event.id, event.admin_email, 'gallery_expired', {
|
|
event_name: event.event_name,
|
|
admin_email: event.admin_email
|
|
});
|
|
|
|
// Start archiving process
|
|
await archiveEvent(event);
|
|
|
|
logger.info(`Handled expiration for event ${event.slug}`);
|
|
} catch (error) {
|
|
logger.error(`Error handling expired event ${event.slug}:`, error);
|
|
}
|
|
}
|
|
|
|
module.exports = { startExpirationChecker };
|