header('X-Inertia'); $wantsJson = ! $isInertia && ($request->expectsJson() || $request->wantsJson()); if (Gate::denies('read')) { // baseline read permission required to generate abort(403); } $request->validate([ 'template_slug' => ['required', 'string', 'exists:document_templates,slug'], 'template_version' => ['nullable', 'integer'], 'custom' => ['nullable', 'array'], 'custom.*' => ['nullable'], ]); // Prefer explicitly requested version if provided and active; otherwise use latest active $baseQuery = DocumentTemplate::query() ->where('slug', $request->template_slug) ->where('core_entity', 'contract') ->where('active', true); if ($request->filled('template_version')) { $template = (clone $baseQuery)->where('version', (int) $request->integer('template_version'))->first(); if (! $template) { $template = (clone $baseQuery)->orderByDesc('version')->firstOrFail(); } } else { $template = $baseQuery->orderByDesc('version')->firstOrFail(); } // Load related data minimally $contract->load(['clientCase.client.person', 'clientCase.person', 'clientCase.client']); $renderer = app(\App\Services\Documents\DocxTemplateRenderer::class); try { // For custom tokens: pass overrides via request bag; service already reads request()->input('custom') if present. $result = $renderer->render($template, $contract, Auth::user()); } catch (\App\Services\Documents\Exceptions\UnresolvedTokensException $e) { if ($wantsJson) { return response()->json([ 'status' => 'error', 'message' => 'Unresolved tokens detected.', 'tokens' => $e->unresolved ?? [], ], 500); } // Return back with validation-like errors so Inertia can surface them via onError return back()->withErrors([ 'document' => 'Unresolved tokens detected.', ])->with('unresolved_tokens', $e->unresolved ?? []); } catch (\Throwable $e) { try { logger()->error('ContractDocumentGenerationController generation failed', [ 'template_id' => $template->id ?? null, 'template_slug' => $template->slug ?? null, 'template_version' => $template->version ?? null, 'error' => $e->getMessage(), ]); } catch (\Throwable $logEx) { } if ($wantsJson) { return response()->json([ 'status' => 'error', 'message' => 'Generation failed.', ], 500); } return back()->withErrors([ 'document' => 'Generation failed.', ]); } $doc = new Document; $doc->fill([ 'uuid' => (string) Str::uuid(), 'name' => $result['fileName'], 'description' => 'Generated from template '.$template->slug.' v'.$template->version, 'user_id' => Auth::id(), 'disk' => 'public', 'path' => $result['relativePath'], 'file_name' => $result['fileName'], 'original_name' => $result['fileName'], 'extension' => 'docx', 'mime_type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'size' => $result['size'], 'checksum' => $result['checksum'], 'is_public' => true, 'template_id' => $template->id, 'template_version' => $template->version, ]); $contract->documents()->save($doc); // Dispatch domain event event(new DocumentGenerated($doc)); // Optional: create an activity if template links to action/decision if (($template->action_id || $template->decision_id || $template->activity_note_template) && $contract->client_case_id) { try { $note = null; if ($template->activity_note_template) { // Interpolate tokens in note using existing resolver logic (non-failing policy: keep) /** @var TokenValueResolver $resolver */ $resolver = app(TokenValueResolver::class); $rawNote = $template->activity_note_template; $tokens = []; if (preg_match_all('/\{([a-zA-Z0-9_\.]+)\}/', $rawNote, $m)) { $tokens = array_unique($m[1]); } $values = []; if ($tokens) { $resolved = $resolver->resolve($tokens, $template, $contract, Auth::user(), 'keep'); foreach ($resolved['values'] as $k => $v) { $values['{'.$k.'}'] = $v; } } $note = strtr($rawNote, $values); } Activity::create(array_filter([ 'note' => $note, 'action_id' => $template->action_id, 'decision_id' => $template->decision_id, 'contract_id' => $contract->id, 'client_case_id' => $contract->client_case_id, ], fn ($v) => ! is_null($v) && $v !== '')); } catch (\Throwable $e) { // swallow activity creation errors to not block document generation } } if ($wantsJson) { return response()->json([ 'status' => 'ok', 'document_uuid' => $doc->uuid, 'path' => $doc->path, 'stats' => $result['stats'] ?? null, 'template' => [ 'id' => $template->id, 'slug' => $template->slug, 'version' => $template->version, 'file_path' => $template->file_path, ], ]); } // Flash some lightweight info if needed by the UI; Inertia will GET the page after redirect return back()->with([ 'doc_generated' => [ 'uuid' => $doc->uuid, 'path' => $doc->path, 'template' => [ 'slug' => $template->slug, 'version' => $template->version, ], 'stats' => $result['stats'] ?? null, ], ]); } }