validate([ 'template_slug' => ['required', 'string', 'exists:document_templates,slug'], 'custom' => ['nullable', 'array'], 'custom.*' => ['nullable'], ]); $template = DocumentTemplate::where('slug', $request->template_slug) ->where('core_entity', 'contract') ->where('active', true) ->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) { return response()->json([ 'status' => 'error', 'message' => 'Unresolved tokens detected.', 'tokens' => $e->unresolved ?? [], ], 422); } catch (\Throwable $e) { return response()->json([ 'status' => 'error', 'message' => 'Generation failed.', ], 500); } $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 } } return response()->json([ 'status' => 'ok', 'document_uuid' => $doc->uuid, 'path' => $doc->path, ]); } }