146 خطوط
9.7 KiB
PHP
146 خطوط
9.7 KiB
PHP
<?php
|
|
|
|
namespace App\Modules\Certificates\Http;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\User;
|
|
use App\Modules\Certificates\Application\IssueCertificate;
|
|
use App\Modules\Courses\Domain\CourseVersion;
|
|
use App\Modules\Identity\Application\RolePermissions;
|
|
use App\Modules\Identity\Domain\Enums\Permission;
|
|
use App\Modules\Tenancy\Application\TenantContext;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Str;
|
|
|
|
final class CertificateController extends Controller
|
|
{
|
|
public function __construct(private readonly TenantContext $tenant, private readonly RolePermissions $permissions, private readonly IssueCertificate $issuer) {}
|
|
|
|
public function index(Request $request): JsonResponse
|
|
{
|
|
$this->authorize($request);
|
|
$items = DB::table('certificates as c')->join('users as u', 'u.id', '=', 'c.user_id')->join('course_versions as cv', 'cv.id', '=', 'c.course_version_id')->where('c.organization_id', $this->tenant->id())->orderByDesc('c.issued_at')->limit(100)->get(['c.id', 'c.certificate_number as number', 'c.verification_code as verificationCode', 'c.issued_at as issuedAt', 'c.expires_at as expiresAt', 'c.revoked_at as revokedAt', 'u.name as learnerName', 'cv.title as courseTitle']);
|
|
$templates = DB::table('certificate_templates')->where('organization_id', $this->tenant->id())->where('is_active', true)->orderByDesc('is_default')->get()->map(fn ($row) => ['id' => $row->id, 'name' => $row->name, 'canvas' => json_decode($row->canvas, true), 'isDefault' => (bool) $row->is_default]);
|
|
$eligible = DB::table('assignment_users as au')->join('assignments as a', 'a.id', '=', 'au.assignment_id')->join('users as u', 'u.id', '=', 'au.user_id')->join('course_versions as cv', 'cv.id', '=', 'a.assignable_id')->where('a.organization_id', $this->tenant->id())->where('a.assignable_type', 'course')->where('au.status', 'completed')->whereNotExists(fn ($query) => $query->selectRaw('1')->from('certificates as c')->whereColumn('c.user_id', 'au.user_id')->whereColumn('c.course_version_id', 'a.assignable_id'))->limit(100)->get(['u.id as userId', 'u.name as learnerName', 'cv.id as courseVersionId', 'cv.title as courseTitle']);
|
|
|
|
return response()->json(['data' => ['items' => $items, 'templates' => $templates, 'eligible' => $eligible]]);
|
|
}
|
|
|
|
public function storeTemplate(Request $request): JsonResponse
|
|
{
|
|
$this->authorize($request);
|
|
$data = $request->validate(['name' => ['required', 'string', 'max:120'], 'canvas' => ['required', 'array'], 'isDefault' => ['nullable', 'boolean']]);
|
|
$id = (string) Str::ulid();
|
|
DB::transaction(function () use ($request, $data, $id): void {
|
|
if ($data['isDefault'] ?? false) {
|
|
DB::table('certificate_templates')->where('organization_id', $this->tenant->id())->update(['is_default' => false]);
|
|
}
|
|
DB::table('certificate_templates')->insert(['id' => $id, 'organization_id' => $this->tenant->id(), 'created_by' => $request->user()->getKey(), 'name' => $data['name'], 'canvas' => json_encode($data['canvas']), 'is_default' => $data['isDefault'] ?? false, 'is_active' => true, 'created_at' => now(), 'updated_at' => now()]);
|
|
});
|
|
|
|
return response()->json(['data' => ['id' => $id]], 201);
|
|
}
|
|
|
|
public function updateTemplate(Request $request, string $template): JsonResponse
|
|
{
|
|
$this->authorize($request);
|
|
$data = $request->validate(['name' => ['required', 'string', 'max:120'], 'canvas' => ['required', 'array'], 'isDefault' => ['nullable', 'boolean']]);
|
|
$exists = DB::table('certificate_templates')->where('organization_id', $this->tenant->id())->where('id', $template)->where('is_active', true)->exists();
|
|
abort_unless($exists, 404);
|
|
DB::transaction(function () use ($data, $template): void {
|
|
if ($data['isDefault'] ?? false) {
|
|
DB::table('certificate_templates')->where('organization_id', $this->tenant->id())->update(['is_default' => false]);
|
|
}
|
|
DB::table('certificate_templates')->where('organization_id', $this->tenant->id())->where('id', $template)->update(['name' => $data['name'], 'canvas' => json_encode($data['canvas']), 'is_default' => $data['isDefault'] ?? false, 'updated_at' => now()]);
|
|
});
|
|
|
|
return response()->json(['data' => ['updated' => true]]);
|
|
}
|
|
|
|
public function archiveTemplate(Request $request, string $template): JsonResponse
|
|
{
|
|
$this->authorize($request);
|
|
$updated = DB::table('certificate_templates')->where('organization_id', $this->tenant->id())->where('id', $template)->where('is_active', true)->update(['is_active' => false, 'is_default' => false, 'updated_at' => now()]);
|
|
abort_unless($updated === 1, 404);
|
|
|
|
return response()->json(['data' => ['archived' => true]]);
|
|
}
|
|
|
|
public function issue(Request $request): JsonResponse
|
|
{
|
|
$this->authorize($request);
|
|
$data = $request->validate(['userId' => ['required', 'string'], 'courseVersionId' => ['required', 'string'], 'templateId' => ['nullable', 'string'], 'expiresInMonths' => ['nullable', 'integer', 'min:1', 'max:120']]);
|
|
$eligible = DB::table('assignment_users as au')->join('assignments as a', 'a.id', '=', 'au.assignment_id')->where('a.organization_id', $this->tenant->id())->where('a.assignable_type', 'course')->where('a.assignable_id', $data['courseVersionId'])->where('au.user_id', $data['userId'])->where('au.status', 'completed')->exists();
|
|
abort_unless($eligible, 422, 'Completion rules are not satisfied for this learner and course version.');
|
|
$learner = User::query()->where('organization_id', $this->tenant->id())->findOrFail($data['userId']);
|
|
$version = CourseVersion::query()->where('organization_id', $this->tenant->id())->findOrFail($data['courseVersionId']);
|
|
$id = $this->issuer->issue($learner, $version, $data['templateId'] ?? null, $data['expiresInMonths'] ?? null);
|
|
|
|
return response()->json(['data' => ['id' => $id]], 201);
|
|
}
|
|
|
|
public function issueBulk(Request $request): JsonResponse
|
|
{
|
|
$this->authorize($request);
|
|
$data = $request->validate(['items' => ['required', 'array', 'min:1', 'max:100'], 'items.*.userId' => ['required', 'string'], 'items.*.courseVersionId' => ['required', 'string'], 'templateId' => ['nullable', 'string'], 'expiresInMonths' => ['nullable', 'integer', 'min:1', 'max:120']]);
|
|
$issued = 0;
|
|
$skipped = 0;
|
|
foreach ($data['items'] as $item) {
|
|
$eligible = DB::table('assignment_users as au')->join('assignments as a', 'a.id', '=', 'au.assignment_id')->where('a.organization_id', $this->tenant->id())->where('a.assignable_type', 'course')->where('a.assignable_id', $item['courseVersionId'])->where('au.user_id', $item['userId'])->where('au.status', 'completed')->exists();
|
|
$exists = DB::table('certificates')->where('organization_id', $this->tenant->id())->where('user_id', $item['userId'])->where('course_version_id', $item['courseVersionId'])->exists();
|
|
if (! $eligible || $exists) {
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
$learner = User::query()->where('organization_id', $this->tenant->id())->find($item['userId']);
|
|
$version = CourseVersion::query()->where('organization_id', $this->tenant->id())->find($item['courseVersionId']);
|
|
if (! $learner || ! $version) {
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
$this->issuer->issue($learner, $version, $data['templateId'] ?? null, $data['expiresInMonths'] ?? null);
|
|
$issued++;
|
|
}
|
|
|
|
return response()->json(['data' => ['issued' => $issued, 'skipped' => $skipped]], 201);
|
|
}
|
|
|
|
public function revoke(Request $request, string $certificate): JsonResponse
|
|
{
|
|
$this->authorize($request);
|
|
$data = $request->validate(['reason' => ['required', 'string', 'max:500']]);
|
|
$updated = DB::table('certificates')->where('organization_id', $this->tenant->id())->where('id', $certificate)->whereNull('revoked_at')->update(['revoked_at' => now(), 'revocation_reason' => $data['reason'], 'updated_at' => now()]);
|
|
abort_unless($updated === 1, 404);
|
|
|
|
return response()->json(['data' => ['revoked' => true]]);
|
|
}
|
|
|
|
public function download(Request $request, string $certificate)
|
|
{
|
|
$this->authorize($request);
|
|
$row = DB::table('certificates')->where('organization_id', $this->tenant->id())->where('id', $certificate)->first();
|
|
abort_unless($row && $row->path && $row->disk && Storage::disk($row->disk)->exists($row->path), 404);
|
|
|
|
return Storage::disk($row->disk)->download($row->path, ($row->certificate_number ?: 'certificate').'.pdf');
|
|
}
|
|
|
|
public function verify(string $code): JsonResponse
|
|
{
|
|
$row = DB::table('certificates as c')->join('users as u', 'u.id', '=', 'c.user_id')->join('course_versions as cv', 'cv.id', '=', 'c.course_version_id')->join('organizations as o', 'o.id', '=', 'c.organization_id')->where('c.verification_code', $code)->first(['c.certificate_number as number', 'c.issued_at as issuedAt', 'c.expires_at as expiresAt', 'c.revoked_at as revokedAt', 'c.revocation_reason as revocationReason', 'u.name as learnerName', 'cv.title as courseTitle', 'o.name as organizationName']);
|
|
abort_unless($row, 404);
|
|
$status = $row->revokedAt ? 'revoked' : ($row->expiresAt && now()->isAfter($row->expiresAt) ? 'expired' : 'valid');
|
|
|
|
return response()->json(['data' => ['status' => $status, ...((array) $row)]]);
|
|
}
|
|
|
|
private function authorize(Request $request): void
|
|
{
|
|
abort_unless($request->user() && $this->permissions->allows($request->user(), Permission::CoursesAuthor), 403);
|
|
}
|
|
}
|