Skip to content

refactor: optimizing dropoutEnrollment command #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
212 changes: 212 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Dropout Enrollments Command

## **Overview**

This console command automates the process of marking enrollments as **DROPOUT** based on specific conditions.

---

## Setup Instructions

### Prerequisites

* Composer
* PHP 8.x
* Laravel 10+
* MySQL

### Clone the Repository

```bash
git clone https://github.com/ashshidiq23/technical-excercise
cd technical-excercise
```

### Install Dependencies

```bash
composer install
```

### Create a `.env` file

Copy the .env.example file to .env

```bash
cp .env.example .env
```

Set up your database connection in .env:

```dotenv
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=your_database
DB_USERNAME=your_username
DB_PASSWORD=your_password
```

### Run Migration & Seeder

```bash
php artisan migrate --seed
```

---

## API Documentation

### Dropout Enrollments Command

This command drops out any enrollments that meet the following criteria:

* The enrollment deadline has passed.
* The student has no active exams (`IN_PROGRESS` status).
* The student has no pending submissions (`WAITING_REVIEW` status).

After that, the command will execute:

1. The enrollment status is updated to `DROPOUT`.
2. An activity log entry is created for tracking.

#### Usage

To execute the command, run:

```bash
php artisan enrollments:dropout
```

example output:

```text
Enrollments to be dropped out: 500000
Excluded from drop out: 39401
Final dropped out enrollments: 460599
default/App\Console\Commands\DropOutEnrollments: 30.00 MiB - 68719 ms
```

## Testing Instructions

#### Verify the dropout process manually:

* Before Running the Command, query enrollments that should be dropped based on the latest deadline:

```sql
SELECT *
FROM enrollments
WHERE deadline_at <= (SELECT deadline_at FROM enrollments ORDER BY id DESC LIMIT 1)
AND status != 'DROPOUT';
```

* Run the command:

```bash
php artisan enrollments:dropout
```

* Check the enrollments table to ensure that the status of the relevant enrollments has been updated to `DROPOUT`:

```sql
SELECT *
FROM enrollments
WHERE status = 'DROPOUT';
```

---

## Product Analysis

### Pain Points

1. **No Learning Progress at a Glance**
- Users cannot quickly see their course progress, making it difficult to track learning milestones.
- Proposed Solution: Add a progress bar, completion percentage, and estimated time left for each course.

2. **Blank Page Before Each Question in Quizzes**
- When loading the next question, a blank page is showing, causing a distracting experience.
- Proposed Solution: Implement smooth transitions using skeleton loading or preloading the next question in the
background or make it client-side entirely.

3. **Price Point is Too Steep**
- The pricing model may not be affordable to all learners.
- Proposed Solution: Introduce tiered pricing (e.g., Basic, Pro, Premium) and differentiate the feature between them
e.g. no certificate in Basic Tier, etc.

### Feature Recommendations, Justification, and Solution Design

1. **AI Implementation**
- **Recommended Learning Path:** AI-driven personalization can help learners choose the most suitable courses based
on their existing skills, learning pace, and career goals. This helps beginner to choose their preferred learning
path.
- **AI Chat in Course Pages:** Many learners struggle with understanding concepts in real-time. An AI-powered
assistant can provide instant explanations, reducing frustration and increasing retention.

**Solution Design:**
- Use LLM to power AI chat.
- Implement a recommendation engine using machine learning to analyze user progress and suggest courses.
- Display AI suggestions on the dashboard in an easy-to-read format.

2. **Gamification for Engagement**
- **Daily Learning Goals:** Encourages consistency, as learners are more likely to stay committed if they have
short-term, achievable goals. Gamification elements have been proven to increase motivation and user engagement (
like Duolingo).
![img.png](public/duo-streak.png)
- **Badges and Achievements:** Visual indicators of progress provide intrinsic motivation, making learners more
likely to complete courses (like badge on google developers or microsoft learn).
- **Skillset Display:** A character sheet-style display helps learners track their growing expertise, reinforcing a
sense of accomplishment and making their progress more tangible.
![img.png](public/skill-card.png)

**Solution Design:**
- Add a user profile section displaying earned badges and skill levels.
- Implement a streak-tracking system that rewards users for consistent learning.
- Use progress bars and experience points to make progress more engaging.

3. **Minor UI Enhancements and Quality of Life Updates**
- Learning Path Menu: Convert the learning path menu into a sidebar format to display all options at once, improving
navigation.
- Learning Path Testimonials: Display testimonials in a carousel format to showcase more reviews efficiently.
- Dicoding Mentoring: Add a filter option to sort mentors based on their expertise, making it easier for learners to
find relevant guidance.
- Rewards Menu: Show accumulated points directly in the Rewards menu, rather than only in the profile hover section.

**Solution Design:**
- Redesign UI components to improve visibility and accessibility.
- Ensure enhancements are lightweight and do not impact site performance.

### Implementation Strategy

To ensure a smooth rollout and faster iteration, features will be released one by one based on priority and user impact.
This allows for incremental improvements, continuous user feedback, and reduced risk.

#### Phase 1: Progress Tracking UI & Performance Optimization

- Develop and test a dashboard update with progress indicators.
- Implement skeleton loaders for quiz questions to reduce flicker.

#### Phase 2: AI Features

- Train an LLM model for AI chat support.
- Develop a recommendation engine for personalized learning paths.

#### Phase 3: Gamification System

- Implement streak tracking, badges, and skillset visualization.
- Develop a user profile section displaying achievements.

#### Phase 4: Full Deployment & Continuous Improvement

- Gradually roll out finalized features platform-wide with proper onboarding.
- Monitor key success metrics and user satisfaction.

*Do A/B testing or beta release on each phase to test the impact of the changes on user engagement and satisfaction.*

---

### Success Metrics

- **User Engagement:** Increase in average session duration and interaction with the platform.
- **Course Completion Rate:** Measure improvement before and after progress tracking implementation.
- **User Satisfaction:** Collect feedback through surveys and track Net Promoter Score (NPS).
87 changes: 51 additions & 36 deletions app/Console/Commands/DropOutEnrollments.php
Original file line number Diff line number Diff line change
Expand Up @@ -67,41 +67,56 @@ public function handle()
*/
private function dropOutEnrollmentsBefore(Carbon $deadline)
{
$enrollmentsToBeDroppedOut = Enrollment::where('deadline_at', '<=', $deadline)->get();

$this->info('Enrollments to be dropped out: ' . count($enrollmentsToBeDroppedOut));
$droppedOutEnrollments = 0;

foreach ($enrollmentsToBeDroppedOut as $enrollment) {
$hasActiveExam = Exam::where('course_id', $enrollment->course_id)
->where('student_id', $enrollment->student_id)
->where('status', 'IN_PROGRESS')
->exists();

$hasWaitingReviewSubmission = Submission::where('course_id', $enrollment->course_id)
->where('student_id', $enrollment->student_id)
->where('status', 'WAITING_REVIEW')
->exists();

if ($hasActiveExam || $hasWaitingReviewSubmission) {
continue;
}

$enrollment->update([
'status' => 'DROPOUT',
'updated_at' => now(),
]);

Activity::create([
'resource_id' => $enrollment->id,
'user_id' => $enrollment->student_id,
'description' => 'COURSE_DROPOUT',
]);

$droppedOutEnrollments++;
}

$this->info('Excluded from drop out: ' . count($enrollmentsToBeDroppedOut) - $droppedOutEnrollments);
$this->info('Final dropped out enrollments: ' . $droppedOutEnrollments);
$initialEnrollmentsToBeDroppedOut = Enrollment::where('enrollments.deadline_at', '<=', $deadline);
$initialEnrollmentsToBeDroppedOutCount = $initialEnrollmentsToBeDroppedOut->count();

$this->info('Enrollments to be dropped out: ' . $initialEnrollmentsToBeDroppedOutCount);

$batchSize = 500;
$enrollmentsToBeDroppedOutCount = 0;
$now = now();
$initialEnrollmentsToBeDroppedOut
->leftJoin('exams', function ($join) {
$join->on('enrollments.course_id', '=', 'exams.course_id')
->on('enrollments.student_id', '=', 'exams.student_id')
->where('exams.status', 'IN_PROGRESS');
})
->leftJoin('submissions', function ($join) {
$join->on('enrollments.course_id', '=', 'submissions.course_id')
->on('enrollments.student_id', '=', 'submissions.student_id')
->where('submissions.status', 'WAITING_REVIEW');
})
->whereNull('exams.id')
->whereNull('submissions.id')
->selectRaw('DISTINCT enrollments.id, enrollments.student_id')
->orderBy('enrollments.id')
->chunkById($batchSize, function ($enrollments) use (&$enrollmentsToBeDroppedOutCount, $now) {
$ids = [];
$activities = [];

foreach ($enrollments as $enrollment) {
$ids[] = $enrollment->id;
$activities[] = [
'resource_id' => $enrollment->id,
'user_id' => $enrollment->student_id,
'description' => 'COURSE_DROPOUT',
];
}

Enrollment::whereIn('id', $ids)->update([
'status' => 'DROPOUT',
'updated_at' => $now,
]);

Activity::insert($activities);
$enrollmentsToBeDroppedOutCount += count($activities);

unset($ids, $activities);
gc_collect_cycles();
}, 'enrollments.id', 'id');

$this->info('Excluded from drop out: ' . $initialEnrollmentsToBeDroppedOutCount - $enrollmentsToBeDroppedOutCount);
$this->info('Final dropped out enrollments: ' . $enrollmentsToBeDroppedOutCount);
}

}
Binary file added public/duo-streak.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/skill-card.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.