validate([ 'operations' => ['required', 'array'], 'operations.*.entity' => ['required', 'string'], 'operations.*.op' => ['required', 'string'], 'operations.*.uuid' => ['required', 'uuid'], 'operations.*.data' => ['required', 'array'], 'operations.*.client_updated_at' => ['nullable', 'date'], ]); $user = $request->user(); $results = []; foreach ($data['operations'] as $op) { $results[] = $this->handle($user, $op); } return response()->json(['results' => $results]); } private function handle(User $user, array $op): array { $uuid = $op['uuid']; try { if ($op['entity'] === 'progress_update' && $op['op'] === 'create') { return $this->progressUpdateCreate($user, $uuid, $op); } return $this->error($uuid, 'unsupported entity/op: ' . $op['entity'] . '.' . $op['op']); } catch (\Throwable $e) { return $this->error($uuid, $e->getMessage()); } } private function progressUpdateCreate(User $user, string $uuid, array $op): array { // Idempotency: same uuid already applied → duplicate (no-op). $existing = ProgressUpdate::where('uuid', $uuid)->first(); if ($existing) { return ['uuid' => $uuid, 'status' => 'duplicate', 'server_id' => $existing->id]; } $validator = Validator::make($op['data'], [ 'phase_id' => ['required', 'integer', 'exists:phases,id'], 'progress' => ['required', 'integer', 'min:0', 'max:100'], 'comment' => ['nullable', 'string'], 'location' => ['nullable', 'array'], ]); if ($validator->fails()) { return $this->error($uuid, 'validation: ' . $validator->errors()->first()); } $d = $validator->validated(); $phase = Phase::with('project')->findOrFail($d['phase_id']); // Authorization: project membership + permission (per-op, never abort the batch). $isMember = $user->can('manage all') || ($phase->project && $phase->project->users()->where('user_id', $user->id)->exists()); if (! $isMember || ! $user->can('update progress')) { return $this->error($uuid, 'forbidden'); } $pu = ProgressUpdate::create([ 'uuid' => $uuid, 'phase_id' => $phase->id, 'user_id' => $user->id, // server-set, never trust client 'progress_percent' => $d['progress'], 'comment' => $d['comment'] ?? null, 'location' => $d['location'] ?? null, 'client_updated_at' => $op['client_updated_at'] ?? null, ]); // Mirror the web behaviour: reflect latest progress on the phase. $phase->progress_percent = $d['progress']; $phase->save(); return ['uuid' => $uuid, 'status' => 'applied', 'server_id' => $pu->id]; } private function error(string $uuid, string $message): array { return ['uuid' => $uuid, 'status' => 'error', 'error' => $message]; } }