const db = require('../db/models'); const ForbiddenError = require('./notifications/errors/forbidden'); const ValidationError = require('./notifications/errors/validation'); const { CAMPUS_ATTENDANCE_DEFAULT_LIMIT, CAMPUS_ATTENDANCE_MANAGER_ROLE_NAMES, CAMPUS_ATTENDANCE_MANAGE_PRODUCT_ROLES, CAMPUS_ATTENDANCE_MAX_LIMIT, CAMPUS_ATTENDANCE_TENANT_WIDE_ROLE_NAMES, getProductRole, normalizeCampusKey, } = require('../constants/campus-attendance'); function getOrganizationId(currentUser) { return currentUser?.organizations?.id || currentUser?.organization?.id || currentUser?.organizationsId || currentUser?.organizationId || null; } function getCampusId(currentUser) { if (Array.isArray(currentUser?.staff_user) && currentUser.staff_user[0]?.campusId) { return currentUser.staff_user[0].campusId; } return currentUser?.campusId || null; } function getCurrentUserCampusKey(currentUser) { const staffProfile = Array.isArray(currentUser?.staff_user) ? currentUser.staff_user[0] : null; return normalizeCampusKey(currentUser?.campus?.code) || normalizeCampusKey(currentUser?.campus?.name) || normalizeCampusKey(staffProfile?.campus?.code) || normalizeCampusKey(staffProfile?.campus?.name) || null; } function getDisplayName(currentUser) { const firstName = currentUser?.firstName || ''; const lastName = currentUser?.lastName || ''; const fullName = `${firstName} ${lastName}`.trim(); return fullName || currentUser?.email || 'Staff Member'; } function getRoleName(currentUser) { return currentUser?.app_role?.name; } function hasTenantWideAccess(currentUser) { return currentUser?.app_role?.globalAccess === true || CAMPUS_ATTENDANCE_TENANT_WIDE_ROLE_NAMES.includes(getRoleName(currentUser)); } function canManageCampusAttendance(currentUser) { return currentUser?.app_role?.globalAccess === true || CAMPUS_ATTENDANCE_MANAGER_ROLE_NAMES.includes(getRoleName(currentUser)) || CAMPUS_ATTENDANCE_MANAGE_PRODUCT_ROLES.includes(getProductRole(currentUser)); } function assertAuthenticatedTenantUser(currentUser) { if (currentUser?.id && getOrganizationId(currentUser)) { return; } throw new ForbiddenError(); } function assertCanManageCampusAttendance(currentUser) { assertAuthenticatedTenantUser(currentUser); if (canManageCampusAttendance(currentUser)) { return; } throw new ForbiddenError(); } function campusKeyFromRoute(value) { const campusKey = normalizeCampusKey(value); if (!campusKey) { throw new ValidationError(); } return campusKey; } function assertCanAccessCampusKey(campusKey, currentUser) { if (hasTenantWideAccess(currentUser)) { return; } const currentCampusKey = getCurrentUserCampusKey(currentUser); if (currentCampusKey && currentCampusKey === campusKey) { return; } throw new ForbiddenError(); } function applyCampusScope(where, filter, currentUser) { const requestedCampusKey = normalizeCampusKey(filter?.campusKey); if (requestedCampusKey) { assertCanAccessCampusKey(requestedCampusKey, currentUser); where.campus_key = requestedCampusKey; return; } if (hasTenantWideAccess(currentUser)) { return; } const currentCampusKey = getCurrentUserCampusKey(currentUser); if (!currentCampusKey) { throw new ForbiddenError(); } where.campus_key = currentCampusKey; } function parseLimit(value) { if (value === undefined) { return CAMPUS_ATTENDANCE_DEFAULT_LIMIT; } const limit = Number(value); if (!Number.isInteger(limit) || limit <= 0) { throw new ValidationError(); } return Math.min(limit, CAMPUS_ATTENDANCE_MAX_LIMIT); } function requiredNonNegativeInteger(value) { if (!Number.isInteger(value) || value < 0) { throw new ValidationError(); } return value; } function optionalText(value) { if (typeof value !== 'string' || value.trim().length === 0) { return null; } return value.trim(); } function requiredDate(value) { if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/u.test(value)) { throw new ValidationError(); } return value; } function validateSummary(data) { if (!data || typeof data !== 'object' || Array.isArray(data)) { throw new ValidationError(); } const totalEnrolled = requiredNonNegativeInteger(data.total_enrolled); const totalPresent = requiredNonNegativeInteger(data.total_present); const totalAbsent = requiredNonNegativeInteger(data.total_absent); const totalTardy = requiredNonNegativeInteger(data.total_tardy || 0); if (totalEnrolled <= 0 || totalPresent > totalEnrolled || totalAbsent > totalEnrolled || totalTardy > totalEnrolled) { throw new ValidationError(); } return { total_enrolled: totalEnrolled, total_present: totalPresent, total_absent: totalAbsent, total_tardy: totalTardy, attendance_percentage: Number(((totalPresent / totalEnrolled) * 100).toFixed(2)), notes: optionalText(data.notes), }; } function toConfigDto(record) { const plainRecord = typeof record.get === 'function' ? record.get({ plain: true }) : record; return { id: plainRecord.id, campus_key: plainRecord.campus_key, attendance_link: plainRecord.attendance_link, updated_by_label: plainRecord.updated_by_label, organizationId: plainRecord.organizationId, campusId: plainRecord.campusId, createdById: plainRecord.createdById, updatedById: plainRecord.updatedById, createdAt: plainRecord.createdAt, updatedAt: plainRecord.updatedAt, }; } function toSummaryDto(record) { const plainRecord = typeof record.get === 'function' ? record.get({ plain: true }) : record; return { id: plainRecord.id, campus_key: plainRecord.campus_key, date: plainRecord.attendance_date, total_enrolled: plainRecord.total_enrolled, total_present: plainRecord.total_present, total_absent: plainRecord.total_absent, total_tardy: plainRecord.total_tardy, attendance_percentage: Number(plainRecord.attendance_percentage), recorded_by_label: plainRecord.recorded_by_label, notes: plainRecord.notes, organizationId: plainRecord.organizationId, campusId: plainRecord.campusId, createdById: plainRecord.createdById, updatedById: plainRecord.updatedById, createdAt: plainRecord.createdAt, updatedAt: plainRecord.updatedAt, }; } module.exports = class CampusAttendanceService { static async listConfigs(filter, currentUser) { assertAuthenticatedTenantUser(currentUser); const where = { organizationId: getOrganizationId(currentUser), }; applyCampusScope(where, filter, currentUser); const result = await db.campus_attendance_config.findAndCountAll({ where, order: [['campus_key', 'asc']], }); return { rows: result.rows.map(toConfigDto), count: result.count, }; } static async upsertConfig(campusKeyParam, data, currentUser) { assertCanManageCampusAttendance(currentUser); const campusKey = campusKeyFromRoute(campusKeyParam); assertCanAccessCampusKey(campusKey, currentUser); if (!data || typeof data !== 'object' || Array.isArray(data)) { throw new ValidationError(); } const attendanceLink = optionalText(data.attendance_link); const where = { organizationId: getOrganizationId(currentUser), campus_key: campusKey, }; const payload = { campus_key: campusKey, attendance_link: attendanceLink, updated_by_label: getDisplayName(currentUser), organizationId: getOrganizationId(currentUser), campusId: getCampusId(currentUser), updatedById: currentUser.id, }; const transaction = await db.sequelize.transaction(); try { const existing = await db.campus_attendance_config.findOne({ where, transaction }); const saved = existing ? await existing.update(payload, { transaction }) : await db.campus_attendance_config.create( { ...payload, createdById: currentUser.id, }, { transaction }, ); await transaction.commit(); return toConfigDto(saved); } catch (error) { await transaction.rollback(); throw error; } } static async listSummaries(filter, currentUser) { assertAuthenticatedTenantUser(currentUser); const where = { organizationId: getOrganizationId(currentUser), }; applyCampusScope(where, filter, currentUser); if (filter.startDate) { where.attendance_date = { ...(where.attendance_date || {}), [db.Sequelize.Op.gte]: requiredDate(filter.startDate), }; } if (filter.endDate) { where.attendance_date = { ...(where.attendance_date || {}), [db.Sequelize.Op.lte]: requiredDate(filter.endDate), }; } const result = await db.campus_attendance_summaries.findAndCountAll({ where, limit: parseLimit(filter.limit), order: [['attendance_date', 'desc'], ['campus_key', 'asc']], }); return { rows: result.rows.map(toSummaryDto), count: result.count, }; } static async upsertSummary(campusKeyParam, dateParam, data, currentUser) { assertCanManageCampusAttendance(currentUser); const campusKey = campusKeyFromRoute(campusKeyParam); const attendanceDate = requiredDate(dateParam); assertCanAccessCampusKey(campusKey, currentUser); const validatedSummary = validateSummary(data); const where = { organizationId: getOrganizationId(currentUser), campus_key: campusKey, attendance_date: attendanceDate, }; const payload = { ...validatedSummary, campus_key: campusKey, attendance_date: attendanceDate, recorded_by_label: getDisplayName(currentUser), organizationId: getOrganizationId(currentUser), campusId: getCampusId(currentUser), updatedById: currentUser.id, }; const transaction = await db.sequelize.transaction(); try { const existing = await db.campus_attendance_summaries.findOne({ where, transaction }); const saved = existing ? await existing.update(payload, { transaction }) : await db.campus_attendance_summaries.create( { ...payload, createdById: currentUser.id, }, { transaction }, ); await transaction.commit(); return toSummaryDto(saved); } catch (error) { await transaction.rollback(); throw error; } } };