All files / web/src/db/schema enrollment-requests.ts

82.05% Statements 128/156
100% Branches 5/5
50% Functions 3/6
82.05% Lines 128/156

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 1572x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 2x 2x 2x 2x 2x 2x 2x 2x 2x                           2x 2x 2x 2x 2x                           2x 2x 2x 2x 2x      
import { createId } from '@paralleldrive/cuid2'
import { index, integer, sqliteTable, text, uniqueIndex } from 'drizzle-orm/sqlite-core'
import { classrooms } from './classrooms'
import { players } from './players'
import { users } from './users'
 
/**
 * Enrollment request status
 */
export type EnrollmentRequestStatus = 'pending' | 'approved' | 'denied' | 'cancelled'
 
/**
 * Who initiated the enrollment request
 */
export type EnrollmentRequestRole = 'parent' | 'teacher'
 
/**
 * Approval status for a single party
 */
export type ApprovalStatus = 'approved' | 'denied'
 
/**
 * Enrollment requests - consent workflow for classroom enrollment
 *
 * Enrollment requires mutual consent:
 * - If parent initiates: teacher must approve
 * - If teacher initiates: any linked parent must approve
 *
 * Once all required approvals are in, the actual enrollment is created
 * and the request status is set to 'approved'.
 */
export const enrollmentRequests = sqliteTable(
  'enrollment_requests',
  {
    /** Primary key */
    id: text('id')
      .primaryKey()
      .$defaultFn(() => createId()),
 
    /** Classroom this request is for */
    classroomId: text('classroom_id')
      .notNull()
      .references(() => classrooms.id, { onDelete: 'cascade' }),
 
    /** Student (player) to be enrolled */
    playerId: text('player_id')
      .notNull()
      .references(() => players.id, { onDelete: 'cascade' }),
 
    // ---- Who initiated ----
 
    /** User who created this request */
    requestedBy: text('requested_by')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
 
    /** Role of the requester */
    requestedByRole: text('requested_by_role').notNull().$type<EnrollmentRequestRole>(),
 
    /** When the request was created */
    requestedAt: integer('requested_at', { mode: 'timestamp' })
      .notNull()
      .$defaultFn(() => new Date()),
 
    // ---- Overall status ----
 
    /** Current status of the request */
    status: text('status').notNull().default('pending').$type<EnrollmentRequestStatus>(),
 
    // ---- Teacher approval ----
 
    /** Teacher's approval decision (null if not yet acted or not required) */
    teacherApproval: text('teacher_approval').$type<ApprovalStatus>(),
 
    /** When teacher approved/denied */
    teacherApprovedAt: integer('teacher_approved_at', { mode: 'timestamp' }),
 
    // ---- Parent approval ----
 
    /** Parent's approval decision (null if not yet acted or not required) */
    parentApproval: text('parent_approval').$type<ApprovalStatus>(),
 
    /** Which parent approved (since multiple parents may exist) */
    parentApprovedBy: text('parent_approved_by').references(() => users.id),
 
    /** When parent approved/denied */
    parentApprovedAt: integer('parent_approved_at', { mode: 'timestamp' }),
 
    // ---- Resolution ----
 
    /** When the request was resolved (approved, denied, or cancelled) */
    resolvedAt: integer('resolved_at', { mode: 'timestamp' }),
  },
  (table) => ({
    /** One active request per player per classroom */
    classroomPlayerIdx: uniqueIndex('idx_enrollment_requests_classroom_player').on(
      table.classroomId,
      table.playerId
    ),
 
    /** Index for finding all requests for a classroom */
    classroomIdx: index('idx_enrollment_requests_classroom').on(table.classroomId),
 
    /** Index for finding all requests for a player */
    playerIdx: index('idx_enrollment_requests_player').on(table.playerId),
 
    /** Index for filtering by status */
    statusIdx: index('idx_enrollment_requests_status').on(table.status),
  })
)
 
export type EnrollmentRequest = typeof enrollmentRequests.$inferSelect
export type NewEnrollmentRequest = typeof enrollmentRequests.$inferInsert
 
/**
 * Determine what approvals are required based on who initiated
 */
export function getRequiredApprovals(
  requestedByRole: EnrollmentRequestRole
): ('teacher' | 'parent')[] {
  switch (requestedByRole) {
    case 'parent':
      // Parent initiated → need teacher approval
      return ['teacher']
    case 'teacher':
      // Teacher initiated → need parent approval
      return ['parent']
    default:
      return []
  }
}
 
/**
 * Check if a request has all required approvals
 */
export function isFullyApproved(request: EnrollmentRequest): boolean {
  const required = getRequiredApprovals(request.requestedByRole as EnrollmentRequestRole)

  for (const role of required) {
    if (role === 'teacher' && request.teacherApproval !== 'approved') {
      return false
    }
    if (role === 'parent' && request.parentApproval !== 'approved') {
      return false
    }
  }

  return true
}
 
/**
 * Check if a request has been denied by anyone
 */
export function isDenied(request: EnrollmentRequest): boolean {
  return request.teacherApproval === 'denied' || request.parentApproval === 'denied'
}