certificateRepo = $certificateRepo; $this->assignmentRepo = $assignmentRepo; $this->definitionRepo = $definitionRepo; $this->cycleCalculator = $cycleCalculator; } public function issueForAssignment(int $assignmentId, int $submissionId): int { $existing = $this->certificateRepo->findByAssignmentId($assignmentId); if ($existing !== null) { return (int) $existing['id']; } $assignment = $this->assignmentRepo->findById($assignmentId); if ($assignment === null) { throw new RuntimeException('Assignment nicht gefunden'); } $task = $this->definitionRepo->findById((int) $assignment['task_id']); if ($task === null) { throw new RuntimeException('Aufgabe nicht gefunden'); } if ((int) $task['certificate_enabled'] !== 1) { return 0; } $validFrom = new DateTimeImmutable(); $validUntil = null; $validityMode = $this->resolveValidityMode($task); if ($validityMode === 'days') { $validityDays = (int) ($task['certificate_validity_days'] ?? 0); if ($validityDays > 0) { $validUntil = $validFrom->modify('+' . $validityDays . ' days')->format('Y-m-d H:i:s'); } } elseif ($validityMode === 'cycle') { $validUntil = $this->resolveCycleValidUntil($task, $assignment, $validFrom); } return $this->certificateRepo->create([ 'task_id' => (int) $assignment['task_id'], 'assignment_id' => $assignmentId, 'submission_id' => $submissionId, 'main_contact_id' => (int) $assignment['main_contact_id'], 'certificate_code' => $this->buildCertificateCode(), 'valid_from' => $validFrom->format('Y-m-d H:i:s'), 'valid_until' => $validUntil, 'status' => 'valid', ]); } public function listUserCertificates(int $mainContactId): array { return $this->certificateRepo->listForUser($mainContactId); } public function listUserTaskCertificates(int $mainContactId, int $taskId): array { if ($taskId <= 0) { return []; } return $this->certificateRepo->listForUserTask($mainContactId, $taskId); } public function getUserTaskCertificateSummary(int $mainContactId, array $taskIds): array { return $this->certificateRepo->summarizeForUserTasks($mainContactId, $taskIds); } public function refreshExpiredCertificates(): int { return $this->certificateRepo->markExpiredByDate((new DateTimeImmutable())->format('Y-m-d H:i:s')); } private function buildCertificateCode(): string { return strtoupper(bin2hex(random_bytes(8))); } private function resolveCycleValidUntil(array $task, array $assignment, DateTimeImmutable $reference): ?string { $assignmentCycleEnd = trim((string) ($assignment['cycle_end_at'] ?? '')); if ($assignmentCycleEnd !== '') { try { return (new DateTimeImmutable($assignmentCycleEnd))->format('Y-m-d H:i:s'); } catch (Throwable $throwable) { // fallback to calculated cycle } } if ((string) ($task['frequency_type'] ?? 'once') !== 'recurring') { return null; } $cycle = $this->cycleCalculator->resolveCurrentCycle($task, $reference); if ($cycle === [] || !($cycle['cycle_end_at'] ?? null) instanceof DateTimeImmutable) { return null; } return $cycle['cycle_end_at']->format('Y-m-d H:i:s'); } private function resolveValidityMode(array $task): string { $mode = (string) ($task['certificate_validity_mode'] ?? ''); if (in_array($mode, ['cycle', 'days', 'permanent'], true)) { return $mode; } $days = (int) ($task['certificate_validity_days'] ?? 0); if ($days > 0) { return 'days'; } if ((string) ($task['frequency_type'] ?? 'once') === 'recurring') { return 'cycle'; } return 'permanent'; } }