226 lines
8.1 KiB
PHP
226 lines
8.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Academy;
|
|
|
|
use App\Models\AcademyCourse;
|
|
use App\Models\AcademyLesson;
|
|
use App\Models\AcademyLessonProgress;
|
|
use App\Models\AcademyPromptTemplate;
|
|
use App\Models\AcademySavedPrompt;
|
|
use App\Models\AcademySave;
|
|
use App\Models\AcademyUserProgress;
|
|
use App\Models\User;
|
|
use App\Support\AcademyAnalytics\AcademyAnalyticsContentType;
|
|
use App\Support\AcademyAnalytics\AcademyAnalyticsEventType;
|
|
use App\Support\AcademyAnalytics\AcademyAnalyticsProgressStatus;
|
|
use Illuminate\Http\Request;
|
|
|
|
final class AcademyProgressService
|
|
{
|
|
public function __construct(
|
|
private readonly AcademyBadgeService $badges,
|
|
private readonly AcademyCourseProgressService $courses,
|
|
private readonly AcademyAnalyticsService $analytics,
|
|
) {
|
|
}
|
|
|
|
public function startLesson(User $user, int $lessonId, ?int $courseId = null, ?Request $request = null): AcademyUserProgress
|
|
{
|
|
$progress = $this->updateUserProgressRecord($user, $courseId, $lessonId, [
|
|
'status' => AcademyAnalyticsProgressStatus::STARTED,
|
|
'progress_percent' => 0,
|
|
'started_at' => now(),
|
|
'completed_at' => null,
|
|
'last_seen_at' => now(),
|
|
]);
|
|
|
|
if ($courseId) {
|
|
$course = AcademyCourse::query()->find($courseId);
|
|
if ($course instanceof AcademyCourse) {
|
|
$this->courses->markEnrollmentStarted($user, $course);
|
|
$lesson = AcademyLesson::query()->find($lessonId);
|
|
if ($lesson instanceof AcademyLesson) {
|
|
$this->courses->updateLastLesson($user, $course, $lesson);
|
|
}
|
|
$this->syncCourseProgressRecord($user, $course);
|
|
}
|
|
}
|
|
|
|
$this->analytics->track([
|
|
'event_type' => AcademyAnalyticsEventType::LESSON_STARTED,
|
|
'content_type' => AcademyAnalyticsContentType::LESSON,
|
|
'content_id' => $lessonId,
|
|
'metadata' => array_filter([
|
|
'course_id' => $courseId,
|
|
], static fn (mixed $value): bool => $value !== null),
|
|
], $user, $request);
|
|
|
|
return $progress;
|
|
}
|
|
|
|
public function completeLesson(User $user, int $lessonId, ?int $courseId = null, ?Request $request = null): AcademyUserProgress
|
|
{
|
|
$progress = $this->updateUserProgressRecord($user, $courseId, $lessonId, [
|
|
'status' => AcademyAnalyticsProgressStatus::COMPLETED,
|
|
'progress_percent' => 100,
|
|
'started_at' => now(),
|
|
'completed_at' => now(),
|
|
'last_seen_at' => now(),
|
|
]);
|
|
|
|
if ($courseId) {
|
|
$course = AcademyCourse::query()->find($courseId);
|
|
if ($course instanceof AcademyCourse) {
|
|
$this->syncCourseProgressRecord($user, $course);
|
|
}
|
|
}
|
|
|
|
$this->analytics->track([
|
|
'event_type' => AcademyAnalyticsEventType::LESSON_COMPLETED,
|
|
'content_type' => AcademyAnalyticsContentType::LESSON,
|
|
'content_id' => $lessonId,
|
|
'metadata' => array_filter([
|
|
'course_id' => $courseId,
|
|
], static fn (mixed $value): bool => $value !== null),
|
|
], $user, $request);
|
|
|
|
return $progress;
|
|
}
|
|
|
|
public function startCourse(User $user, int $courseId, ?Request $request = null): AcademyUserProgress
|
|
{
|
|
$course = AcademyCourse::query()->findOrFail($courseId);
|
|
$this->courses->markEnrollmentStarted($user, $course);
|
|
|
|
$progress = $this->syncCourseProgressRecord($user, $course, true);
|
|
|
|
$this->analytics->track([
|
|
'event_type' => AcademyAnalyticsEventType::COURSE_STARTED,
|
|
'content_type' => AcademyAnalyticsContentType::COURSE,
|
|
'content_id' => $courseId,
|
|
], $user, $request);
|
|
|
|
return $progress;
|
|
}
|
|
|
|
public function completeCourse(User $user, int $courseId, ?Request $request = null): AcademyUserProgress
|
|
{
|
|
$course = AcademyCourse::query()->findOrFail($courseId);
|
|
$this->courses->markCourseCompletedIfFinished($user, $course);
|
|
$progress = $this->syncCourseProgressRecord($user, $course);
|
|
|
|
if ($progress->status === AcademyAnalyticsProgressStatus::COMPLETED) {
|
|
$this->analytics->track([
|
|
'event_type' => AcademyAnalyticsEventType::COURSE_COMPLETED,
|
|
'content_type' => AcademyAnalyticsContentType::COURSE,
|
|
'content_id' => $courseId,
|
|
], $user, $request);
|
|
}
|
|
|
|
return $progress;
|
|
}
|
|
|
|
public function markLessonComplete(User $user, AcademyLesson $lesson, ?AcademyCourse $course = null, ?Request $request = null): AcademyLessonProgress
|
|
{
|
|
$progress = AcademyLessonProgress::query()->updateOrCreate(
|
|
[
|
|
'lesson_id' => $lesson->id,
|
|
'user_id' => $user->id,
|
|
],
|
|
[
|
|
'completed_at' => now(),
|
|
],
|
|
);
|
|
|
|
if ($course instanceof AcademyCourse) {
|
|
$this->courses->updateLastLesson($user, $course, $lesson);
|
|
$this->courses->markCourseCompletedIfFinished($user, $course);
|
|
}
|
|
|
|
$this->completeLesson($user, (int) $lesson->id, $course?->id, $request);
|
|
|
|
$this->badges->syncForUser($user);
|
|
|
|
return $progress;
|
|
}
|
|
|
|
public function savePrompt(User $user, AcademyPromptTemplate $prompt): AcademySavedPrompt
|
|
{
|
|
$saved = AcademySavedPrompt::query()->firstOrCreate([
|
|
'prompt_template_id' => $prompt->id,
|
|
'user_id' => $user->id,
|
|
]);
|
|
|
|
AcademySave::query()->firstOrCreate([
|
|
'user_id' => $user->id,
|
|
'content_type' => AcademyAnalyticsContentType::PROMPT,
|
|
'content_id' => $prompt->id,
|
|
]);
|
|
|
|
$this->analytics->track([
|
|
'event_type' => AcademyAnalyticsEventType::PROMPT_SAVE,
|
|
'content_type' => AcademyAnalyticsContentType::PROMPT,
|
|
'content_id' => (int) $prompt->id,
|
|
], $user);
|
|
|
|
$this->badges->syncForUser($user);
|
|
|
|
return $saved;
|
|
}
|
|
|
|
public function unsavePrompt(User $user, AcademyPromptTemplate $prompt): void
|
|
{
|
|
AcademySavedPrompt::query()
|
|
->where('prompt_template_id', $prompt->id)
|
|
->where('user_id', $user->id)
|
|
->delete();
|
|
|
|
AcademySave::query()
|
|
->where('user_id', $user->id)
|
|
->where('content_type', AcademyAnalyticsContentType::PROMPT)
|
|
->where('content_id', $prompt->id)
|
|
->delete();
|
|
|
|
$this->badges->syncForUser($user);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $attributes
|
|
*/
|
|
private function updateUserProgressRecord(User $user, ?int $courseId, ?int $lessonId, array $attributes): AcademyUserProgress
|
|
{
|
|
return AcademyUserProgress::query()->updateOrCreate(
|
|
[
|
|
'user_id' => $user->id,
|
|
'course_id' => $courseId,
|
|
'lesson_id' => $lessonId,
|
|
],
|
|
$attributes,
|
|
);
|
|
}
|
|
|
|
private function syncCourseProgressRecord(User $user, AcademyCourse $course, bool $forceStarted = false): AcademyUserProgress
|
|
{
|
|
$progressPercent = $this->courses->getProgressPercent($user, $course);
|
|
$isComplete = $this->courses->getTotalRequiredLessonsCount($course) > 0 && $progressPercent >= 100;
|
|
$status = $isComplete
|
|
? AcademyAnalyticsProgressStatus::COMPLETED
|
|
: ($progressPercent > 0 || $forceStarted
|
|
? AcademyAnalyticsProgressStatus::IN_PROGRESS
|
|
: AcademyAnalyticsProgressStatus::STARTED);
|
|
|
|
return $this->updateUserProgressRecord($user, (int) $course->id, null, [
|
|
'status' => $status,
|
|
'progress_percent' => $progressPercent,
|
|
'started_at' => now(),
|
|
'completed_at' => $isComplete ? now() : null,
|
|
'last_seen_at' => now(),
|
|
'metadata' => [
|
|
'completed_required' => $this->courses->getCompletedRequiredLessonsCount($user, $course),
|
|
'total_required' => $this->courses->getTotalRequiredLessonsCount($course),
|
|
],
|
|
]);
|
|
}
|
|
} |