Skip to content

Commit 8c52a09

Browse files
authored
Merge pull request #3401 from codeeu/dev
Dev
2 parents 36ff221 + 2bc0c1a commit 8c52a09

3 files changed

Lines changed: 188 additions & 4 deletions

File tree

app/CertificateExcellence.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ public function generate()
6565
return $s3path;
6666
}
6767

68+
/**
69+
* Dry-run style preflight: compile the certificate locally without S3 upload.
70+
* Cleans up temp files regardless of success/failure.
71+
*/
72+
public function preflight(): void
73+
{
74+
try {
75+
$this->customize_and_save_latex();
76+
$this->run_pdf_creation();
77+
} finally {
78+
$this->clean_temp_files();
79+
}
80+
}
81+
6882
/**
6983
* Clean up LaTeX artifacts for the generated file.
7084
*/
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\CertificateExcellence;
6+
use App\Excellence;
7+
use Illuminate\Console\Command;
8+
9+
class CertificatePreflight extends Command
10+
{
11+
protected $signature = 'certificate:preflight
12+
{--edition=2025 : Target edition year}
13+
{--type=all : excellence|super-organiser|all}
14+
{--limit=0 : Max records to test (0 = all)}
15+
{--only-pending : Test only rows without certificate_url}
16+
{--export= : Optional CSV path for failures}';
17+
18+
protected $description = 'Dry-run compile certificates (no S3 upload, no DB updates) and report failures';
19+
20+
public function handle(): int
21+
{
22+
$edition = (int) $this->option('edition');
23+
$typeOption = strtolower(trim((string) $this->option('type')));
24+
$limit = max(0, (int) $this->option('limit'));
25+
$onlyPending = (bool) $this->option('only-pending');
26+
$exportPath = trim((string) $this->option('export'));
27+
28+
$types = $this->resolveTypes($typeOption);
29+
if ($types === null) {
30+
$this->error("Invalid --type value: {$typeOption}. Use 'excellence', 'super-organiser', or 'all'.");
31+
return self::FAILURE;
32+
}
33+
34+
$query = Excellence::query()
35+
->where('edition', $edition)
36+
->whereIn('type', $types)
37+
->with('user')
38+
->orderBy('type')
39+
->orderBy('id');
40+
41+
if ($onlyPending) {
42+
$query->whereNull('certificate_url');
43+
}
44+
45+
if ($limit > 0) {
46+
$query->limit($limit);
47+
}
48+
49+
$rows = $query->get();
50+
if ($rows->isEmpty()) {
51+
$this->info('No recipients found for the selected filters.');
52+
return self::SUCCESS;
53+
}
54+
55+
$failures = [];
56+
$ok = 0;
57+
$bar = $this->output->createProgressBar($rows->count());
58+
$bar->start();
59+
60+
foreach ($rows as $e) {
61+
$bar->advance();
62+
$user = $e->user;
63+
if (! $user) {
64+
$failures[] = $this->failureRow($e, '', 'Missing related user record.');
65+
continue;
66+
}
67+
if (! $user->email) {
68+
$failures[] = $this->failureRow($e, (string) $user->email, 'Missing user email.');
69+
continue;
70+
}
71+
72+
$name = $e->name_for_certificate ?? trim(($user->firstname ?? '') . ' ' . ($user->lastname ?? ''));
73+
if ($name === '') {
74+
$failures[] = $this->failureRow($e, (string) $user->email, 'Empty certificate holder name.');
75+
continue;
76+
}
77+
78+
$certType = $e->type === 'SuperOrganiser' ? 'super-organiser' : 'excellence';
79+
$numberOfActivities = $e->type === 'SuperOrganiser' ? (int) $user->activities($edition) : 0;
80+
81+
try {
82+
$cert = new CertificateExcellence(
83+
$edition,
84+
$name,
85+
$certType,
86+
$numberOfActivities,
87+
(int) $user->id,
88+
(string) $user->email
89+
);
90+
$cert->preflight();
91+
$ok++;
92+
} catch (\Throwable $ex) {
93+
$failures[] = $this->failureRow($e, (string) $user->email, $ex->getMessage());
94+
}
95+
}
96+
$bar->finish();
97+
$this->newLine(2);
98+
99+
$this->info("Preflight complete. Tested: {$rows->count()}, Passed: {$ok}, Failed: " . count($failures));
100+
101+
if (! empty($failures)) {
102+
$show = array_slice($failures, 0, 20);
103+
$this->table(['id', 'type', 'user_id', 'email', 'name', 'error'], $show);
104+
if (count($failures) > 20) {
105+
$this->line('Showing first 20 failures. Use --export for full list.');
106+
}
107+
}
108+
109+
if ($exportPath !== '') {
110+
$path = $this->resolvePath($exportPath);
111+
$dir = dirname($path);
112+
if (! is_dir($dir) && ! @mkdir($dir, 0775, true) && ! is_dir($dir)) {
113+
$this->error("Failed to create export directory: {$dir}");
114+
return self::FAILURE;
115+
}
116+
$fh = @fopen($path, 'wb');
117+
if (! $fh) {
118+
$this->error("Failed to open export file: {$path}");
119+
return self::FAILURE;
120+
}
121+
fputcsv($fh, ['id', 'type', 'edition', 'user_id', 'email', 'name_for_certificate', 'error']);
122+
foreach ($failures as $row) {
123+
fputcsv($fh, [
124+
$row['id'],
125+
$row['type'],
126+
$edition,
127+
$row['user_id'],
128+
$row['email'],
129+
$row['name'],
130+
$row['error'],
131+
]);
132+
}
133+
fclose($fh);
134+
$this->info("Exported failures CSV: {$path}");
135+
}
136+
137+
return self::SUCCESS;
138+
}
139+
140+
private function resolveTypes(string $typeOption): ?array
141+
{
142+
return match ($typeOption) {
143+
'all' => ['Excellence', 'SuperOrganiser'],
144+
'excellence' => ['Excellence'],
145+
'super-organiser', 'superorganiser' => ['SuperOrganiser'],
146+
default => null,
147+
};
148+
}
149+
150+
private function resolvePath(string $path): string
151+
{
152+
if (str_starts_with($path, '/')) {
153+
return $path;
154+
}
155+
return base_path($path);
156+
}
157+
158+
private function failureRow(Excellence $e, string $email, string $error): array
159+
{
160+
$name = $e->name_for_certificate ?? ($e->user ? trim(($e->user->firstname ?? '') . ' ' . ($e->user->lastname ?? '')) : '');
161+
return [
162+
'id' => $e->id,
163+
'type' => $e->type,
164+
'user_id' => (int) $e->user_id,
165+
'email' => $email,
166+
'name' => (string) $name,
167+
'error' => $error,
168+
];
169+
}
170+
}

resources/views/certificates.blade.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080

8181
@if(!$excellence->isEmpty())
8282
@foreach($excellence as $certificate_of_excellence)
83-
@if(!is_null($certificate_of_excellence->name_for_certificate))
83+
@if(!empty($certificate_of_excellence->certificate_url))
8484
<tr class="{{ $loop->even ? 'bg-[#F5F2FA]' : 'bg-white' }}">
8585
<td class="border-r border-[#B399D6] px-6 py-4 font-semibold text-xl">
8686
<span class="text-slate-500 font-semibold">Excellence</span>
@@ -112,7 +112,7 @@
112112

113113
@if(!$superOrganiser->isEmpty())
114114
@foreach($superOrganiser as $super_organiser_certificate)
115-
@if(!is_null($super_organiser_certificate->name_for_certificate))
115+
@if(!empty($super_organiser_certificate->certificate_url))
116116
<tr class="{{ $loop->even ? 'bg-[#F5F2FA]' : 'bg-white' }}">
117117
<td class="border-r border-[#B399D6] px-6 py-4 font-semibold text-xl">
118118
<span class="text-slate-500 font-semibold">Super Organiser</span>
@@ -222,7 +222,7 @@
222222

223223
@if(!$excellence->isEmpty())
224224
@foreach($excellence as $certificate_of_excellence)
225-
@if(!is_null($certificate_of_excellence->name_for_certificate))
225+
@if(!empty($certificate_of_excellence->certificate_url))
226226
<div class="border-2 border-[#B399D6] rounded-lg overflow-hidden">
227227
<div class="flex">
228228
<div class="flex items-center px-4 py-5 bg-[#410098] border-r border-b border-[#B399D6] font-['Montserrat'] font-semibold text-base text-white w-[108px]">
@@ -272,7 +272,7 @@
272272

273273
@if(!$superOrganiser->isEmpty())
274274
@foreach($superOrganiser as $super_organiser_certificate)
275-
@if(!is_null($super_organiser_certificate->name_for_certificate))
275+
@if(!empty($super_organiser_certificate->certificate_url))
276276
<div class="border-2 border-[#B399D6] rounded-lg overflow-hidden">
277277
<div class="flex">
278278
<div class="flex items-center px-4 py-5 bg-[#410098] border-r border-b border-[#B399D6] font-['Montserrat'] font-semibold text-base text-white w-[108px]">

0 commit comments

Comments
 (0)