Teren-app/app/Http/Controllers/ContractDocumentGenerationController.php

180 lines
7.3 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Events\DocumentGenerated;
use App\Models\Activity;
use App\Models\Contract;
use App\Models\Document;
use App\Models\DocumentTemplate;
use App\Services\Documents\TokenValueResolver;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
class ContractDocumentGenerationController extends Controller
{
public function __invoke(Request $request, Contract $contract): Response|RedirectResponse
{
// Inertia requests include the X-Inertia header and should receive redirects or Inertia responses, not JSON
$isInertia = (bool) $request->header('X-Inertia');
// For non-Inertia POSTs, prefer JSON responses by default (including tests)
$wantsJson = ! $isInertia;
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,
],
]);
}
}