Skip to content

feat: add batch process for updating status of classes and enrolled_at per ticket 607#1164

Open
carddev81 wants to merge 1 commit into
mainfrom
carddev81/ticket_id607_createbatchprocess
Open

feat: add batch process for updating status of classes and enrolled_at per ticket 607#1164
carddev81 wants to merge 1 commit into
mainfrom
carddev81/ticket_id607_createbatchprocess

Conversation

@carddev81

Copy link
Copy Markdown
Contributor

Pre-Submission PR Checklist

  • No debug/console/fmt.Println statements
  • Unnecessary development comments removed
  • All acceptance criteria verified
  • Functions according to ticket specifications
  • Tested manually where applicable
  • [] Branch rebased with latest main
  • No business logic exists within the database layer

Description of the change

Added batch process that runs at 5AM every morning that will flip a classes status from Scheduled to Active when the class start date has either passed or is the current day of the run also the job searches for program_class_enrollments within that class and will set the enrolled_at to the current day of the run.

NOTE: this is an easy way to run the job, you just need to obtain the job id from the crons table.

nats pub --server=nats://127.0.0.1:4222 --user=unlocked --password=dev tasks.activate_scheduled_classes '{"job_id":"626fc009-47ad-48f2-8222-fed55ea523b7"}'

To test please create a class set to Scheduled and start date is yesterday or some day in the past. Enroll students. Then kick off job.

@carddev81 carddev81 requested a review from a team as a code owner June 7, 2026 14:52
@carddev81 carddev81 requested review from calisio and removed request for a team June 7, 2026 14:52
@coderabbitai

coderabbitai Bot commented Jun 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features
    • Added automatic daily activation of scheduled program classes at 5 AM. Classes scheduled to start today are now activated automatically based on facility timezone.
    • Enrollment records are automatically backfilled when classes are activated to ensure complete enrollment data.

Walkthrough

This PR adds a system-wide scheduled job that activates program classes whose start date arrives and backfills enrollment data. It introduces a new SystemJob category, defines the ActivateScheduledClassesJob type with a 5 AM daily schedule, integrates system tasks into the scheduler, wires NATS message subscriptions, and implements class activation with timezone-aware date checking and enrollment timestamp backfilling.

Changes

Scheduled Program Class Activation

Layer / File(s) Summary
System job type definitions
backend/src/models/jobs.go
SystemJob category, ActivateScheduledClassesJob type, and EveryMorningAt5AM schedule are added as constants. The exported AllSystemJobs list references the new job type. CronJob.BeforeCreate classifies jobs in AllSystemJobs as system jobs and applies their schedules via switch case.
Scheduler system task generation
backend/src/tasks/scheduler.go
Scheduler.generateTasks integrates system jobs by calling generateSystemTasks, which creates cron job records and generates pending RunnableTask entries. intoSystemTask ensures provider-less system tasks are persisted and linked to their cron jobs.
NATS subscription and message handler
provider-middleware/handlers.go, provider-middleware/program_classes.go
The initSubscription method wires ActivateScheduledClassesJob to NATS. handleActivateScheduledClasses unmarshals incoming messages, validates the job ID, invokes the activation routine, and reports completion status.
Scheduled class activation and enrollment backfill
provider-middleware/program_classes.go
activateScheduledClasses queries program classes with start dates due today (using facility timezones), updates their status to Active, and backfills ProgramClassEnrollment.enrolled_at timestamps and system user IDs for enrolled entries. systemBatchUserID helper retrieves the system batch user for enrollment updates.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding a batch process for updating class status and enrolled_at timestamps, and references ticket 607.
Description check ✅ Passed The description is well-related to the changeset, providing clear context about the batch process functionality, implementation details, testing instructions, and linking to the related Asana ticket.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@provider-middleware/program_classes.go`:
- Around line 62-68: The Set("class_ids", classIDs) call must remain because
models.ProgramClass.AfterUpdate reads tx.Get("class_ids") to drive enrollment
updates; remove or change the redundant ProgramClassEnrollment update in
provider-middleware/program_classes.go (the explicit update that backfills
enrolled_at and update_user_id after the Transaction) so it does not duplicate
the AfterUpdate hook's backfill for enrolled_at, and instead rely on the hook to
set enrolled_at for enrollment_status = Enrolled where enrolled_at IS NULL; if
you still need to update update_user_id for those rows, modify the hook
(models.ProgramClass.AfterUpdate) to also set update_user_id when it backfills
enrolled_at so the single path performs both updates atomically.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c211ba7a-654f-4445-9d96-06abadcc7bf9

📥 Commits

Reviewing files that changed from the base of the PR and between 5f9c489 and 4445b82.

📒 Files selected for processing (4)
  • backend/src/models/jobs.go
  • backend/src/tasks/scheduler.go
  • provider-middleware/handlers.go
  • provider-middleware/program_classes.go

Comment on lines +62 to +68
if err := sh.db.WithContext(batchCtx).Transaction(func(tx *gorm.DB) error {
if err := tx.
Model(&models.ProgramClass{}).
Where("id IN ?", classIDs).
Set("class_ids", classIDs).
Updates(map[string]any{"status": models.Active}).Error; err != nil {
return err

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Search for ProgramClass AfterUpdate hook that might backfill enrolled_at

rg -n -C5 'func.*ProgramClass.*AfterUpdate' --type go

Repository: UnlockedLabs/UnlockEdv2

Length of output: 860


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find where Set("class_ids", ...) is used
rg -n --hidden --no-ignore -S 'Set\("class_ids"\s*,\s*' .

# 2) Locate provider-middleware/program_classes.go if present and show around the targeted lines
# (Prefer exact filename match; otherwise search for the transaction pattern)
if [ -f "provider-middleware/program_classes.go" ]; then
  echo "---- provider-middleware/program_classes.go ----"
  nl -ba provider-middleware/program_classes.go | sed -n '40,100p'
else
  echo "---- searching for provider-middleware/program_classes.go ----"
  fd -t f -e go --hidden --no-ignore 'program_classes.go' -a . | head -n 50 || true
  # fallback: search for the transaction snippet structure
  rg -n --hidden --no-ignore -S 'WithContext\(batchCtx\)\.Transaction' . || true
fi

# 3) Show full AfterUpdate hook body (limited range around discovered location)
HOOK_FILE="backend/src/models/program_classes.go"
if [ -f "$HOOK_FILE" ]; then
  echo "---- $HOOK_FILE: AfterUpdate ----"
  # print from ~60 lines before hook to ~120 lines after (cap output)
  nl -ba "$HOOK_FILE" | sed -n '50,170p'
fi

Repository: UnlockedLabs/UnlockEdv2

Length of output: 331


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Print with line numbers using `cat -n` (no `nl` available in sandbox)
print_range() {
  local file="$1"
  local start="$2"
  local end="$3"
  echo "---- $file:$start-$end ----"
  cat -n "$file" | sed -n "${start},${end}p"
}

# provider-middleware: inspect around the Set("class_ids", ...)
print_range "provider-middleware/program_classes.go" 40 110

# model hook: inspect full AfterUpdate body
print_range "backend/src/models/program_classes.go" 60 170

# database/enrollments: context for another Set("class_ids", ...)
if [ -f "backend/src/database/class_enrollments.go" ]; then
  print_range "backend/src/database/class_enrollments.go" 230 330
fi

Repository: UnlockedLabs/UnlockEdv2

Length of output: 10652


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find DatabaseFields definition and any hooks/callbacks that might auto-set update_user_id/update_user_id behavior
rg -n --hidden --no-ignore -S 'type DatabaseFields|func \\(.*DatabaseFields\\).*Before|func \\(.*DatabaseFields\\).*After|UpdateUserID|update_user_id' backend/src/models

# Find any hooks specifically on ProgramClassEnrollment for BeforeUpdate/AfterUpdate
rg -n --hidden --no-ignore -S 'type ProgramClassEnrollment|func \\(e \\*ProgramClassEnrollment\\) (BeforeUpdate|AfterUpdate|BeforeSave|AfterSave|BeforeCreate|AfterCreate)' backend/src/models

Repository: UnlockedLabs/UnlockEdv2

Length of output: 1107


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show DatabaseFields implementation around line ~1-120
echo "---- backend/src/models/users.go: DatabaseFields ----"
cat -n backend/src/models/users.go | sed -n '1,130p'

# Search for ProgramClassEnrollment hooks beyond the one we saw (BeforeCreate)
echo "---- ProgramClassEnrollment hooks ----"
rg -n --hidden --no-ignore -S 'func \\(e \\*ProgramClassEnrollment\\) (BeforeUpdate|AfterUpdate|BeforeSave|AfterSave|BeforeCreate|AfterCreate|BeforeUpdate)' backend/src/models/program_classes.go || true

# Show ProgramClassEnrollment hook body section we already partially saw (extend range)
echo "---- backend/src/models/program_classes.go: ProgramClassEnrollment hooks ----"
cat -n backend/src/models/program_classes.go | sed -n '120,220p'

Repository: UnlockedLabs/UnlockEdv2

Length of output: 9199


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Print continuation of ProgramClassEnrollment.BeforeUpdate
cat -n backend/src/models/program_classes.go | sed -n '160,320p'

Repository: UnlockedLabs/UnlockEdv2

Length of output: 7052


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Print continuation of ProgramClassEnrollment.BeforeUpdate
cat -n backend/src/models/program_classes.go | sed -n '160,320p'

Repository: UnlockedLabs/UnlockEdv2

Length of output: 7052


Clarify Set("class_ids", classIDs) purpose and avoid overlapping enrolled_at backfill

  • Set("class_ids", classIDs) is required: models.ProgramClass.AfterUpdate calls tx.Get("class_ids") and uses it to update ProgramClassEnrollment rows when status changes to Active.
  • The AfterUpdate hook already backfills ProgramClassEnrollment.enrolled_at for enrollment_status = Enrolled where enrolled_at IS NULL, so the subsequent explicit ProgramClassEnrollment update in provider-middleware/program_classes.go (lines 70–78) is redundant for enrolled_at and may also skip setting update_user_id due to the enrolled_at IS NULL predicate running after the hook.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@provider-middleware/program_classes.go` around lines 62 - 68, The
Set("class_ids", classIDs) call must remain because
models.ProgramClass.AfterUpdate reads tx.Get("class_ids") to drive enrollment
updates; remove or change the redundant ProgramClassEnrollment update in
provider-middleware/program_classes.go (the explicit update that backfills
enrolled_at and update_user_id after the Transaction) so it does not duplicate
the AfterUpdate hook's backfill for enrolled_at, and instead rely on the hook to
set enrolled_at for enrollment_status = Enrolled where enrolled_at IS NULL; if
you still need to update update_user_id for those rows, modify the hook
(models.ProgramClass.AfterUpdate) to also set update_user_id when it backfills
enrolled_at so the single path performs both updates atomically.

@corypride corypride requested review from corypride and removed request for calisio June 11, 2026 19:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant