diff --git a/app/CertificateExcellence.php b/app/CertificateExcellence.php index a61a5c6ee..c394bda2a 100644 --- a/app/CertificateExcellence.php +++ b/app/CertificateExcellence.php @@ -65,6 +65,20 @@ public function generate() return $s3path; } + /** + * Dry-run style preflight: compile the certificate locally without S3 upload. + * Cleans up temp files regardless of success/failure. + */ + public function preflight(): void + { + try { + $this->customize_and_save_latex(); + $this->run_pdf_creation(); + } finally { + $this->clean_temp_files(); + } + } + /** * Clean up LaTeX artifacts for the generated file. */ diff --git a/app/Console/Commands/CertificatePreflight.php b/app/Console/Commands/CertificatePreflight.php new file mode 100644 index 000000000..1b7915d3a --- /dev/null +++ b/app/Console/Commands/CertificatePreflight.php @@ -0,0 +1,170 @@ +option('edition'); + $typeOption = strtolower(trim((string) $this->option('type'))); + $limit = max(0, (int) $this->option('limit')); + $onlyPending = (bool) $this->option('only-pending'); + $exportPath = trim((string) $this->option('export')); + + $types = $this->resolveTypes($typeOption); + if ($types === null) { + $this->error("Invalid --type value: {$typeOption}. Use 'excellence', 'super-organiser', or 'all'."); + return self::FAILURE; + } + + $query = Excellence::query() + ->where('edition', $edition) + ->whereIn('type', $types) + ->with('user') + ->orderBy('type') + ->orderBy('id'); + + if ($onlyPending) { + $query->whereNull('certificate_url'); + } + + if ($limit > 0) { + $query->limit($limit); + } + + $rows = $query->get(); + if ($rows->isEmpty()) { + $this->info('No recipients found for the selected filters.'); + return self::SUCCESS; + } + + $failures = []; + $ok = 0; + $bar = $this->output->createProgressBar($rows->count()); + $bar->start(); + + foreach ($rows as $e) { + $bar->advance(); + $user = $e->user; + if (! $user) { + $failures[] = $this->failureRow($e, '', 'Missing related user record.'); + continue; + } + if (! $user->email) { + $failures[] = $this->failureRow($e, (string) $user->email, 'Missing user email.'); + continue; + } + + $name = $e->name_for_certificate ?? trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? '')); + if ($name === '') { + $failures[] = $this->failureRow($e, (string) $user->email, 'Empty certificate holder name.'); + continue; + } + + $certType = $e->type === 'SuperOrganiser' ? 'super-organiser' : 'excellence'; + $numberOfActivities = $e->type === 'SuperOrganiser' ? (int) $user->activities($edition) : 0; + + try { + $cert = new CertificateExcellence( + $edition, + $name, + $certType, + $numberOfActivities, + (int) $user->id, + (string) $user->email + ); + $cert->preflight(); + $ok++; + } catch (\Throwable $ex) { + $failures[] = $this->failureRow($e, (string) $user->email, $ex->getMessage()); + } + } + $bar->finish(); + $this->newLine(2); + + $this->info("Preflight complete. Tested: {$rows->count()}, Passed: {$ok}, Failed: " . count($failures)); + + if (! empty($failures)) { + $show = array_slice($failures, 0, 20); + $this->table(['id', 'type', 'user_id', 'email', 'name', 'error'], $show); + if (count($failures) > 20) { + $this->line('Showing first 20 failures. Use --export for full list.'); + } + } + + if ($exportPath !== '') { + $path = $this->resolvePath($exportPath); + $dir = dirname($path); + if (! is_dir($dir) && ! @mkdir($dir, 0775, true) && ! is_dir($dir)) { + $this->error("Failed to create export directory: {$dir}"); + return self::FAILURE; + } + $fh = @fopen($path, 'wb'); + if (! $fh) { + $this->error("Failed to open export file: {$path}"); + return self::FAILURE; + } + fputcsv($fh, ['id', 'type', 'edition', 'user_id', 'email', 'name_for_certificate', 'error']); + foreach ($failures as $row) { + fputcsv($fh, [ + $row['id'], + $row['type'], + $edition, + $row['user_id'], + $row['email'], + $row['name'], + $row['error'], + ]); + } + fclose($fh); + $this->info("Exported failures CSV: {$path}"); + } + + return self::SUCCESS; + } + + private function resolveTypes(string $typeOption): ?array + { + return match ($typeOption) { + 'all' => ['Excellence', 'SuperOrganiser'], + 'excellence' => ['Excellence'], + 'super-organiser', 'superorganiser' => ['SuperOrganiser'], + default => null, + }; + } + + private function resolvePath(string $path): string + { + if (str_starts_with($path, '/')) { + return $path; + } + return base_path($path); + } + + private function failureRow(Excellence $e, string $email, string $error): array + { + $name = $e->name_for_certificate ?? ($e->user ? trim(($e->user->firstname ?? '') . ' ' . ($e->user->lastname ?? '')) : ''); + return [ + 'id' => $e->id, + 'type' => $e->type, + 'user_id' => (int) $e->user_id, + 'email' => $email, + 'name' => (string) $name, + 'error' => $error, + ]; + } +} diff --git a/resources/views/certificates.blade.php b/resources/views/certificates.blade.php index 8326088e6..9c3205503 100644 --- a/resources/views/certificates.blade.php +++ b/resources/views/certificates.blade.php @@ -80,7 +80,7 @@ @if(!$excellence->isEmpty()) @foreach($excellence as $certificate_of_excellence) - @if(!is_null($certificate_of_excellence->name_for_certificate)) + @if(!empty($certificate_of_excellence->certificate_url))