102 lines
4.7 KiB
PHP
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.');
|
|
}
|
|
}
|