Files
HickLab3D_Dashboard/app/Http/Controllers/ProjectCurrentRevisionController.php
T

102 lines
4.7 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Exceptions\OptimisticLockConflictException;
use App\Models\Project;
use App\Models\ProjectEvent;
use App\Models\ProjectRevision;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
/**
* The project's "confirmed current revision" is a separate pointer from the
* per-listing liveRelease pointers on project_listing_links: it marks which
* revision is the project's current source of truth independent of what is
* released to any particular Etsy listing. Uploading a new revision never
* moves this pointer; only an explicit confirm or clear does, under an
* optimistic concurrency check on the project's own lock_version. Nothing
* here ever mutates a revision row — it is purely a pointer update, so an
* older revision can always be re-confirmed ("rollback") without overwriting
* any immutable history.
*/
class ProjectCurrentRevisionController extends Controller
{
public function update(Request $request, Project $project, ProjectRevision $revision): RedirectResponse
{
abort_unless($revision->project_id === $project->id, 404);
abort_unless($request->user()->can('confirmCurrentRevision', $project), 403);
$data = $request->validate([
'lock_version' => ['required', 'integer', 'min:0'],
]);
try {
DB::transaction(function () use ($request, $project, $revision, $data): void {
$locked = Project::query()->whereKey($project->id)->lockForUpdate()->firstOrFail();
if ((int) $locked->lock_version !== (int) $data['lock_version']) {
throw new OptimisticLockConflictException('The project changed since you loaded it.');
}
$lockedRevision = ProjectRevision::query()->whereKey($revision->id)->firstOrFail();
if ($lockedRevision->project_id !== $locked->id) {
abort(404);
}
if (! $lockedRevision->isApproved()) {
throw ValidationException::withMessages(['revision_id' => 'Only an approved revision can be confirmed as current.']);
}
if ($lockedRevision->files()->count() === 0) {
throw ValidationException::withMessages(['revision_id' => 'An approved revision must have at least one file before it can be confirmed as current.']);
}
$previousId = $locked->current_revision_id;
$locked->update([
'current_revision_id' => $lockedRevision->id,
'lock_version' => $locked->lock_version + 1,
]);
ProjectEvent::record($locked->id, 'current_revision_confirmed', $request->user()->id, ProjectRevision::class, $lockedRevision->id, [
'previous_current_revision_id' => $previousId,
'new_current_revision_id' => $lockedRevision->id,
]);
});
} catch (OptimisticLockConflictException $exception) {
return back()->withErrors(['lock_version' => $exception->getMessage()])->setStatusCode(409);
}
return redirect()->route('projects.show', $project)->with('status', 'Current revision confirmed.');
}
public function destroy(Request $request, Project $project): RedirectResponse
{
abort_unless($request->user()->can('confirmCurrentRevision', $project), 403);
$data = $request->validate([
'lock_version' => ['required', 'integer', 'min:0'],
]);
try {
DB::transaction(function () use ($request, $project, $data): void {
$locked = Project::query()->whereKey($project->id)->lockForUpdate()->firstOrFail();
if ((int) $locked->lock_version !== (int) $data['lock_version']) {
throw new OptimisticLockConflictException('The project changed since you loaded it.');
}
$previousId = $locked->current_revision_id;
$locked->update(['current_revision_id' => null, 'lock_version' => $locked->lock_version + 1]);
ProjectEvent::record($locked->id, 'current_revision_cleared', $request->user()->id, metadata: [
'previous_current_revision_id' => $previousId,
'new_current_revision_id' => null,
]);
});
} catch (OptimisticLockConflictException $exception) {
return back()->withErrors(['lock_version' => $exception->getMessage()])->setStatusCode(409);
}
return redirect()->route('projects.show', $project)->with('status', 'Current revision cleared.');
}
}