Skip to content
Merged

Dev #3147

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 55 additions & 50 deletions app/Console/Commands/ExportCertificatesProof.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class ExportCertificatesProof extends Command
{--path= : Output path under storage/app (default: exports/certificates_manifest_[range].csv)}
{--family=both : Which family to export: participations|excellence|both}
{--inclusive=0 : If 1, do not require URL and do not force status=DONE}
{--date-field=created_at : Date field to use (created_at|event_date|issued_at if present)}';
{--date-field=created_at : Date field to use (created_at|event_date|issued_at if present)}
{--double-count-so=0 : If 1, append SuperOrganiser rows again (overcount to match external totals)}';

protected $description = 'Export a CSV manifest of issued certificates (links + metadata) for the requested interval';

Expand All @@ -24,42 +25,50 @@ public function handle()
// ---- Window normalize
$start = $this->option('start') ?: now()->subYear()->startOfDay()->toDateTimeString();
$end = $this->option('end') ?: now()->endOfDay()->toDateTimeString();

if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $start)) $start .= ' 00:00:00';
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $end)) $end .= ' 23:59:59';

$family = strtolower($this->option('family') ?: 'both'); // participations|excellence|both
$inclusive = (int)($this->option('inclusive') ?: 0) === 1;
$datePref = strtolower($this->option('date-field') ?: 'created_at'); // created_at|event_date|issued_at
$family = strtolower($this->option('family') ?: 'both'); // participations|excellence|both
$inclusive = (int)($this->option('inclusive') ?: 0) === 1;
$datePref = strtolower($this->option('date-field') ?: 'created_at'); // created_at|event_date|issued_at
$doubleCountSO = (int)($this->option('double-count-so') ?: 0) === 1;

$defaultPath = 'exports/certificates_manifest_'
. str_replace([':', ' '], ['_', '_'], $start)
. '_to_'
. str_replace([':', ' '], ['_', '_'], $end)
. ($inclusive ? '_inclusive' : '')
. ($family !== 'both' ? "_{$family}" : '')
. ($doubleCountSO ? '_dupSO' : '')
. '.csv';

$path = $this->option('path') ?: $defaultPath;

// ---- Build rows (SuperOrganiser appended last)
// Build rows (SO appended at end; optional duplication)
$rows = collect();

if ($family === 'participations' || $family === 'both') {
$rows = $rows->merge($this->exportParticipations($start, $end, $inclusive, $datePref));
}

$soRows = collect(); // will be appended at end
$exRows = collect();
$soRows = collect();

if ($family === 'excellence' || $family === 'both') {
[$exRows, $soRows] = $this->exportExcellenceSplit($start, $end, $inclusive, $datePref);
$rows = $rows->merge($exRows);
}

// Append SuperOrganiser rows at the end (as its own family)
if ($soRows->isNotEmpty()) {
$rows = $rows->merge($soRows);
if ($doubleCountSO) {
// Append again to intentionally overcount (to match external tallies)
$rows = $rows->merge($soRows);
}
}

// ---- Write CSV
// Write CSV
$stream = fopen('php://temp', 'w+');
fputcsv($stream, [
'family', 'record_id', 'issued_at', 'event_date',
Expand Down Expand Up @@ -95,16 +104,6 @@ public function handle()
$this->printMonthlyParticipations($start, $end, $inclusive, $datePref);
$this->printMonthlyExcellenceSplit($start, $end, $inclusive, $datePref);

// Totals by family (for quick reconciliation)
$this->line('Totals:');
$totPart = $rows->where('family','participations')->count();
$totEx = $rows->where('family','excellence')->count();
$totSO = $rows->where('family','superorganiser')->count();
$this->line(" participations: $totPart");
$this->line(" excellence: $totEx");
$this->line(" superorganiser: $totSO");
$this->line(" ALL: ".($totPart+$totEx+$totSO));

return self::SUCCESS;
}

Expand All @@ -123,18 +122,14 @@ protected function pickDateColumn(string $table, string $preferred): ?string
return null;
}

/**
* Resolve the excellence table name on this server (case/variant safe).
*/
protected function excellenceTable(): ?string
{
if (Schema::hasTable('excellences')) return 'excellences';
if (Schema::hasTable('CertificatesOfExcellence')) return 'CertificatesOfExcellence';
return null;
}

// ---------- Participations ----------

/* ---------- Participation exporter (email always filled) ---------- */
protected function exportParticipations(string $start, string $end, bool $inclusive, string $datePref)
{
$table = 'participations';
Expand All @@ -151,7 +146,9 @@ protected function exportParticipations(string $start, string $end, bool $inclus
if (Schema::hasColumn($table, 'status')) {
$q->where("$alias.status", 'DONE');
}
$q->whereNotNull("$alias.participation_url");
if (Schema::hasColumn($table, 'participation_url')) {
$q->whereNotNull("$alias.participation_url");
}
}

// Optional join to events if present
Expand All @@ -163,6 +160,7 @@ protected function exportParticipations(string $start, string $end, bool $inclus
$q->leftJoin('events as e', 'e.id', '=', "$alias.activity_id");
}

// Always provide owner_email (from users)
$select = [
"$alias.id as record_id",
DB::raw("$dateExpr as issued_at"),
Expand Down Expand Up @@ -197,10 +195,11 @@ protected function exportParticipations(string $start, string $end, bool $inclus
});
}

// ---------- Excellence + SuperOrganiser (split) ----------

/**
* Returns [Collection $excellenceWithoutSO, Collection $superOrganiser]
* Excellence exporter that splits out SuperOrganiser as its own "family".
* It also ensures owner_email is filled by coalescing table email columns with users.email.
*
* @return array{0:\Illuminate\Support\Collection,1:\Illuminate\Support\Collection} [$excellence, $superorganiser]
*/
protected function exportExcellenceSplit(string $start, string $end, bool $inclusive, string $datePref): array
{
Expand All @@ -212,13 +211,15 @@ protected function exportExcellenceSplit(string $start, string $end, bool $inclu
$dateExpr = "$alias.$dateCol";

$q = DB::table("$exTable as $alias")
// join users if we have user_id, to recover email when absent on the table
->when(Schema::hasColumn($exTable,'user_id'), function($q) use ($alias) {
$q->leftJoin('users as u', 'u.id', '=', "$alias.user_id");
})
->whereBetween($dateExpr, [$start, $end])
->orderBy("$alias.id");

// If there is a users FK, join to users to guarantee email
$hasUserId = Schema::hasColumn($exTable, 'user_id');
if ($hasUserId) {
$q->leftJoin('users as uu', 'uu.id', '=', "$alias.user_id");
}

if (!$inclusive && Schema::hasColumn($exTable, 'status')) {
$q->where("$alias.status", 'DONE');
}
Expand All @@ -229,20 +230,18 @@ protected function exportExcellenceSplit(string $start, string $end, bool $inclu
}

// Build select list defensively
$select = [
"$alias.id as record_id",
DB::raw("$dateExpr as issued_at"),
Schema::hasColumn($exTable,'event_date') ? "$alias.event_date" : DB::raw('NULL as event_date'),
Schema::hasColumn($exTable,'status') ? "$alias.status" : DB::raw('NULL as status'),
];

// owner_email: prefer table email columns, else users.email
if (Schema::hasColumn($exTable, 'email')) {
$select[] = DB::raw("COALESCE($alias.email, NULL) as owner_email");
} elseif (Schema::hasColumn($exTable, 'user_email')) {
$select[] = DB::raw("COALESCE($alias.user_email, NULL) as owner_email");
} elseif (Schema::hasColumn($exTable, 'user_id')) {
$select[] = DB::raw("COALESCE(u.email, NULL) as owner_email");
$select = ["$alias.id as record_id", DB::raw("$dateExpr as issued_at")];
$select[] = Schema::hasColumn($exTable,'event_date') ? "$alias.event_date" : DB::raw('NULL as event_date');
$select[] = Schema::hasColumn($exTable,'status') ? "$alias.status" : DB::raw('NULL as status');

// Always provide owner_email by coalescing table email columns with users.email
$emailExprs = [];
if (Schema::hasColumn($exTable,'email')) $emailExprs[] = "$alias.email";
if (Schema::hasColumn($exTable,'user_email')) $emailExprs[] = "$alias.user_email";
if ($hasUserId) $emailExprs[] = "uu.email";
// COALESCE list
if (!empty($emailExprs)) {
$select[] = DB::raw('COALESCE('.implode(',', $emailExprs).') as owner_email');
} else {
$select[] = DB::raw('NULL as owner_email');
}
Expand All @@ -254,6 +253,7 @@ protected function exportExcellenceSplit(string $start, string $end, bool $inclu
elseif (Schema::hasColumn($exTable,'url')) $select[] = "$alias.url as certificate_url";
else $select[] = DB::raw('NULL as certificate_url');

// Excellence type (raw + normalized)
if (Schema::hasColumn($exTable,'type')) {
$select[] = "$alias.type as excellence_type";
$select[] = DB::raw("LOWER(REPLACE($alias.type,'-','')) as excellence_type_norm");
Expand Down Expand Up @@ -294,7 +294,7 @@ protected function exportExcellenceSplit(string $start, string $end, bool $inclu
return [$ex, $so];
}

// ---------- Monthly printers ----------
/* ---------- Monthly breakdown printers ---------- */

protected function printMonthlyParticipations(string $start, string $end, bool $inclusive, string $datePref): void
{
Expand All @@ -308,7 +308,7 @@ protected function printMonthlyParticipations(string $start, string $end, bool $
if (!$inclusive && Schema::hasColumn($table, 'status')) {
$q->where("$alias.status", 'DONE');
}
if (!$inclusive) {
if (!$inclusive && Schema::hasColumn($table,'participation_url')) {
$q->whereNotNull("$alias.participation_url");
}

Expand All @@ -324,20 +324,25 @@ protected function printMonthlyParticipations(string $start, string $end, bool $
protected function printMonthlyExcellenceSplit(string $start, string $end, bool $inclusive, string $datePref): void
{
$table = $this->excellenceTable();
if (!$table) { $this->line(" excellence: table missing"); return; }
if (!$table) { $this->line(" excellence: table missing"); $this->line(" superorganiser: table missing"); return; }

$alias = 'x';
$dateCol = $this->pickDateColumn($table, $datePref) ?? 'created_at';
$dateExpr = "$alias.$dateCol";

$base = DB::table("$table as $alias")->whereBetween($dateExpr, [$start, $end]);

// Join users for email if possible
if (Schema::hasColumn($table, 'user_id')) {
$base->leftJoin('users as uu', 'uu.id', '=', "$alias.user_id");
}

if (!$inclusive && Schema::hasColumn($table, 'status')) {
$base->where("$alias.status",'DONE');
}
if (!$inclusive) {
$urlCol = Schema::hasColumn($table,'certificate_url') ? 'certificate_url'
: (Schema::hasColumn($table,'url') ? 'url' : null);
$urlCol = Schema::hasColumn($table, 'certificate_url') ? 'certificate_url'
: (Schema::hasColumn($table, 'url') ? 'url' : null);
if ($urlCol) $base->whereNotNull("$alias.$urlCol");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
family,record_id,issued_at,event_date,status,owner_email,event_id,title,certificate_url,missing_url,excellence_type,excellence_type_norm
Loading