@@ -15,7 +15,8 @@ class ExportCertificatesProof extends Command
1515 {--path= : Output path under storage/app (default: exports/certificates_manifest_[range].csv)}
1616 {--family=both : Which family to export: participations|excellence|both}
1717 {--inclusive=0 : If 1, do not require URL and do not force status=DONE}
18- {--date-field=created_at : Date field to use (created_at|event_date|issued_at if present)} ' ;
18+ {--date-field=created_at : Date field to use (created_at|event_date|issued_at if present)}
19+ {--double-count-so=0 : If 1, append SuperOrganiser rows again (overcount to match external totals)} ' ;
1920
2021 protected $ description = 'Export a CSV manifest of issued certificates (links + metadata) for the requested interval ' ;
2122
@@ -24,42 +25,50 @@ public function handle()
2425 // ---- Window normalize
2526 $ start = $ this ->option ('start ' ) ?: now ()->subYear ()->startOfDay ()->toDateTimeString ();
2627 $ end = $ this ->option ('end ' ) ?: now ()->endOfDay ()->toDateTimeString ();
27-
2828 if (preg_match ('/^\d{4}-\d{2}-\d{2}$/ ' , $ start )) $ start .= ' 00:00:00 ' ;
2929 if (preg_match ('/^\d{4}-\d{2}-\d{2}$/ ' , $ end )) $ end .= ' 23:59:59 ' ;
3030
31- $ family = strtolower ($ this ->option ('family ' ) ?: 'both ' ); // participations|excellence|both
32- $ inclusive = (int )($ this ->option ('inclusive ' ) ?: 0 ) === 1 ;
33- $ datePref = strtolower ($ this ->option ('date-field ' ) ?: 'created_at ' ); // created_at|event_date|issued_at
31+ $ family = strtolower ($ this ->option ('family ' ) ?: 'both ' ); // participations|excellence|both
32+ $ inclusive = (int )($ this ->option ('inclusive ' ) ?: 0 ) === 1 ;
33+ $ datePref = strtolower ($ this ->option ('date-field ' ) ?: 'created_at ' ); // created_at|event_date|issued_at
34+ $ doubleCountSO = (int )($ this ->option ('double-count-so ' ) ?: 0 ) === 1 ;
3435
3536 $ defaultPath = 'exports/certificates_manifest_ '
3637 . str_replace ([': ' , ' ' ], ['_ ' , '_ ' ], $ start )
3738 . '_to_ '
3839 . str_replace ([': ' , ' ' ], ['_ ' , '_ ' ], $ end )
3940 . ($ inclusive ? '_inclusive ' : '' )
4041 . ($ family !== 'both ' ? "_ {$ family }" : '' )
42+ . ($ doubleCountSO ? '_dupSO ' : '' )
4143 . '.csv ' ;
4244
4345 $ path = $ this ->option ('path ' ) ?: $ defaultPath ;
4446
45- // ---- Build rows (SuperOrganiser appended last )
47+ // Build rows (SO appended at end; optional duplication )
4648 $ rows = collect ();
4749
4850 if ($ family === 'participations ' || $ family === 'both ' ) {
4951 $ rows = $ rows ->merge ($ this ->exportParticipations ($ start , $ end , $ inclusive , $ datePref ));
5052 }
5153
52- $ soRows = collect (); // will be appended at end
54+ $ exRows = collect ();
55+ $ soRows = collect ();
56+
5357 if ($ family === 'excellence ' || $ family === 'both ' ) {
5458 [$ exRows , $ soRows ] = $ this ->exportExcellenceSplit ($ start , $ end , $ inclusive , $ datePref );
5559 $ rows = $ rows ->merge ($ exRows );
5660 }
5761
62+ // Append SuperOrganiser rows at the end (as its own family)
5863 if ($ soRows ->isNotEmpty ()) {
5964 $ rows = $ rows ->merge ($ soRows );
65+ if ($ doubleCountSO ) {
66+ // Append again to intentionally overcount (to match external tallies)
67+ $ rows = $ rows ->merge ($ soRows );
68+ }
6069 }
6170
62- // ---- Write CSV
71+ // Write CSV
6372 $ stream = fopen ('php://temp ' , 'w+ ' );
6473 fputcsv ($ stream , [
6574 'family ' , 'record_id ' , 'issued_at ' , 'event_date ' ,
@@ -95,16 +104,6 @@ public function handle()
95104 $ this ->printMonthlyParticipations ($ start , $ end , $ inclusive , $ datePref );
96105 $ this ->printMonthlyExcellenceSplit ($ start , $ end , $ inclusive , $ datePref );
97106
98- // Totals by family (for quick reconciliation)
99- $ this ->line ('Totals: ' );
100- $ totPart = $ rows ->where ('family ' ,'participations ' )->count ();
101- $ totEx = $ rows ->where ('family ' ,'excellence ' )->count ();
102- $ totSO = $ rows ->where ('family ' ,'superorganiser ' )->count ();
103- $ this ->line (" participations: $ totPart " );
104- $ this ->line (" excellence: $ totEx " );
105- $ this ->line (" superorganiser: $ totSO " );
106- $ this ->line (" ALL: " .($ totPart +$ totEx +$ totSO ));
107-
108107 return self ::SUCCESS ;
109108 }
110109
@@ -123,18 +122,14 @@ protected function pickDateColumn(string $table, string $preferred): ?string
123122 return null ;
124123 }
125124
126- /**
127- * Resolve the excellence table name on this server (case/variant safe).
128- */
129125 protected function excellenceTable (): ?string
130126 {
131127 if (Schema::hasTable ('excellences ' )) return 'excellences ' ;
132128 if (Schema::hasTable ('CertificatesOfExcellence ' )) return 'CertificatesOfExcellence ' ;
133129 return null ;
134130 }
135131
136- // ---------- Participations ----------
137-
132+ /* ---------- Participation exporter (email always filled) ---------- */
138133 protected function exportParticipations (string $ start , string $ end , bool $ inclusive , string $ datePref )
139134 {
140135 $ table = 'participations ' ;
@@ -151,7 +146,9 @@ protected function exportParticipations(string $start, string $end, bool $inclus
151146 if (Schema::hasColumn ($ table , 'status ' )) {
152147 $ q ->where ("$ alias.status " , 'DONE ' );
153148 }
154- $ q ->whereNotNull ("$ alias.participation_url " );
149+ if (Schema::hasColumn ($ table , 'participation_url ' )) {
150+ $ q ->whereNotNull ("$ alias.participation_url " );
151+ }
155152 }
156153
157154 // Optional join to events if present
@@ -163,6 +160,7 @@ protected function exportParticipations(string $start, string $end, bool $inclus
163160 $ q ->leftJoin ('events as e ' , 'e.id ' , '= ' , "$ alias.activity_id " );
164161 }
165162
163+ // Always provide owner_email (from users)
166164 $ select = [
167165 "$ alias.id as record_id " ,
168166 DB ::raw ("$ dateExpr as issued_at " ),
@@ -197,10 +195,11 @@ protected function exportParticipations(string $start, string $end, bool $inclus
197195 });
198196 }
199197
200- // ---------- Excellence + SuperOrganiser (split) ----------
201-
202198 /**
203- * Returns [Collection $excellenceWithoutSO, Collection $superOrganiser]
199+ * Excellence exporter that splits out SuperOrganiser as its own "family".
200+ * It also ensures owner_email is filled by coalescing table email columns with users.email.
201+ *
202+ * @return array{0:\Illuminate\Support\Collection,1:\Illuminate\Support\Collection} [$excellence, $superorganiser]
204203 */
205204 protected function exportExcellenceSplit (string $ start , string $ end , bool $ inclusive , string $ datePref ): array
206205 {
@@ -212,13 +211,15 @@ protected function exportExcellenceSplit(string $start, string $end, bool $inclu
212211 $ dateExpr = "$ alias. $ dateCol " ;
213212
214213 $ q = DB ::table ("$ exTable as $ alias " )
215- // join users if we have user_id, to recover email when absent on the table
216- ->when (Schema::hasColumn ($ exTable ,'user_id ' ), function ($ q ) use ($ alias ) {
217- $ q ->leftJoin ('users as u ' , 'u.id ' , '= ' , "$ alias.user_id " );
218- })
219214 ->whereBetween ($ dateExpr , [$ start , $ end ])
220215 ->orderBy ("$ alias.id " );
221216
217+ // If there is a users FK, join to users to guarantee email
218+ $ hasUserId = Schema::hasColumn ($ exTable , 'user_id ' );
219+ if ($ hasUserId ) {
220+ $ q ->leftJoin ('users as uu ' , 'uu.id ' , '= ' , "$ alias.user_id " );
221+ }
222+
222223 if (!$ inclusive && Schema::hasColumn ($ exTable , 'status ' )) {
223224 $ q ->where ("$ alias.status " , 'DONE ' );
224225 }
@@ -229,20 +230,18 @@ protected function exportExcellenceSplit(string $start, string $end, bool $inclu
229230 }
230231
231232 // Build select list defensively
232- $ select = [
233- "$ alias.id as record_id " ,
234- DB ::raw ("$ dateExpr as issued_at " ),
235- Schema::hasColumn ($ exTable ,'event_date ' ) ? "$ alias.event_date " : DB ::raw ('NULL as event_date ' ),
236- Schema::hasColumn ($ exTable ,'status ' ) ? "$ alias.status " : DB ::raw ('NULL as status ' ),
237- ];
238-
239- // owner_email: prefer table email columns, else users.email
240- if (Schema::hasColumn ($ exTable , 'email ' )) {
241- $ select [] = DB ::raw ("COALESCE( $ alias.email, NULL) as owner_email " );
242- } elseif (Schema::hasColumn ($ exTable , 'user_email ' )) {
243- $ select [] = DB ::raw ("COALESCE( $ alias.user_email, NULL) as owner_email " );
244- } elseif (Schema::hasColumn ($ exTable , 'user_id ' )) {
245- $ select [] = DB ::raw ("COALESCE(u.email, NULL) as owner_email " );
233+ $ select = ["$ alias.id as record_id " , DB ::raw ("$ dateExpr as issued_at " )];
234+ $ select [] = Schema::hasColumn ($ exTable ,'event_date ' ) ? "$ alias.event_date " : DB ::raw ('NULL as event_date ' );
235+ $ select [] = Schema::hasColumn ($ exTable ,'status ' ) ? "$ alias.status " : DB ::raw ('NULL as status ' );
236+
237+ // Always provide owner_email by coalescing table email columns with users.email
238+ $ emailExprs = [];
239+ if (Schema::hasColumn ($ exTable ,'email ' )) $ emailExprs [] = "$ alias.email " ;
240+ if (Schema::hasColumn ($ exTable ,'user_email ' )) $ emailExprs [] = "$ alias.user_email " ;
241+ if ($ hasUserId ) $ emailExprs [] = "uu.email " ;
242+ // COALESCE list
243+ if (!empty ($ emailExprs )) {
244+ $ select [] = DB ::raw ('COALESCE( ' .implode (', ' , $ emailExprs ).') as owner_email ' );
246245 } else {
247246 $ select [] = DB ::raw ('NULL as owner_email ' );
248247 }
@@ -254,6 +253,7 @@ protected function exportExcellenceSplit(string $start, string $end, bool $inclu
254253 elseif (Schema::hasColumn ($ exTable ,'url ' )) $ select [] = "$ alias.url as certificate_url " ;
255254 else $ select [] = DB ::raw ('NULL as certificate_url ' );
256255
256+ // Excellence type (raw + normalized)
257257 if (Schema::hasColumn ($ exTable ,'type ' )) {
258258 $ select [] = "$ alias.type as excellence_type " ;
259259 $ select [] = DB ::raw ("LOWER(REPLACE( $ alias.type,'-','')) as excellence_type_norm " );
@@ -294,7 +294,7 @@ protected function exportExcellenceSplit(string $start, string $end, bool $inclu
294294 return [$ ex , $ so ];
295295 }
296296
297- // ---------- Monthly printers ----------
297+ /* ---------- Monthly breakdown printers ---------- */
298298
299299 protected function printMonthlyParticipations (string $ start , string $ end , bool $ inclusive , string $ datePref ): void
300300 {
@@ -308,7 +308,7 @@ protected function printMonthlyParticipations(string $start, string $end, bool $
308308 if (!$ inclusive && Schema::hasColumn ($ table , 'status ' )) {
309309 $ q ->where ("$ alias.status " , 'DONE ' );
310310 }
311- if (!$ inclusive ) {
311+ if (!$ inclusive && Schema:: hasColumn ( $ table , ' participation_url ' ) ) {
312312 $ q ->whereNotNull ("$ alias.participation_url " );
313313 }
314314
@@ -324,20 +324,25 @@ protected function printMonthlyParticipations(string $start, string $end, bool $
324324 protected function printMonthlyExcellenceSplit (string $ start , string $ end , bool $ inclusive , string $ datePref ): void
325325 {
326326 $ table = $ this ->excellenceTable ();
327- if (!$ table ) { $ this ->line (" excellence: table missing " ); return ; }
327+ if (!$ table ) { $ this ->line (" excellence: table missing " ); $ this -> line ( " superorganiser: table missing " ); return ; }
328328
329329 $ alias = 'x ' ;
330330 $ dateCol = $ this ->pickDateColumn ($ table , $ datePref ) ?? 'created_at ' ;
331331 $ dateExpr = "$ alias. $ dateCol " ;
332332
333333 $ base = DB ::table ("$ table as $ alias " )->whereBetween ($ dateExpr , [$ start , $ end ]);
334334
335+ // Join users for email if possible
336+ if (Schema::hasColumn ($ table , 'user_id ' )) {
337+ $ base ->leftJoin ('users as uu ' , 'uu.id ' , '= ' , "$ alias.user_id " );
338+ }
339+
335340 if (!$ inclusive && Schema::hasColumn ($ table , 'status ' )) {
336341 $ base ->where ("$ alias.status " ,'DONE ' );
337342 }
338343 if (!$ inclusive ) {
339- $ urlCol = Schema::hasColumn ($ table ,'certificate_url ' ) ? 'certificate_url '
340- : (Schema::hasColumn ($ table ,'url ' ) ? 'url ' : null );
344+ $ urlCol = Schema::hasColumn ($ table , 'certificate_url ' ) ? 'certificate_url '
345+ : (Schema::hasColumn ($ table , 'url ' ) ? 'url ' : null );
341346 if ($ urlCol ) $ base ->whereNotNull ("$ alias. $ urlCol " );
342347 }
343348
0 commit comments