Fix 500 generation: include account entity in template defaults and merge global whitelist entities during resolution

This commit is contained in:
Simon Pocrnjič 2025-10-06 19:35:09 +02:00
parent 18fb04fe65
commit 0c8d1e0b5d
2 changed files with 182 additions and 6 deletions

View File

@ -0,0 +1,170 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Http\Requests\StoreDocumentTemplateRequest;
use App\Http\Requests\UpdateDocumentTemplateRequest;
use App\Models\DocumentTemplate;
use App\Services\Documents\TokenScanner;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Inertia\Inertia;
class DocumentTemplateController extends Controller
{
public function index()
{
$this->ensurePermission();
$templates = DocumentTemplate::query()->orderByDesc('updated_at')->get();
return Inertia::render('Admin/DocumentTemplates/Index', [
'templates' => $templates,
]);
}
public function toggleActive(DocumentTemplate $template)
{
$this->ensurePermission();
$template->active = ! $template->active;
$template->updated_by = Auth::id();
$template->save();
return redirect()->back()->with('success', 'Status predloge posodobljen.');
}
public function updateSettings(UpdateDocumentTemplateRequest $request, DocumentTemplate $template)
{
$this->ensurePermission();
$template->fill($request->only(['output_filename_pattern', 'date_format']));
if ($request->has('fail_on_unresolved')) {
$template->fail_on_unresolved = (bool) $request->boolean('fail_on_unresolved');
}
// Build formatting options array from discrete fields if provided
$fmt = $template->formatting_options ?? [];
$dirty = false;
foreach ([
'number_decimals', 'decimal_separator', 'thousands_separator',
'currency_symbol', 'currency_position',
] as $key) {
if ($request->filled($key)) {
$fmt[$key] = $request->input($key);
$dirty = true;
} elseif ($request->has($key) && $request->input($key) === null) {
unset($fmt[$key]);
$dirty = true;
}
}
if ($request->has('currency_space')) {
$fmt['currency_space'] = (bool) $request->boolean('currency_space');
$dirty = true;
}
if ($request->filled('default_date_format')) {
$fmt['default_date_format'] = $request->input('default_date_format');
$dirty = true;
}
if ($request->has('date_formats')) {
$fmt['date_formats'] = array_filter((array) $request->input('date_formats'), fn ($v) => $v !== null && $v !== '');
$dirty = true;
}
if ($dirty) {
$template->formatting_options = $fmt;
}
$template->updated_by = Auth::id();
$template->save();
return redirect()->back()->with('success', 'Nastavitve predloge shranjene.');
}
public function store(StoreDocumentTemplateRequest $request)
{
$this->ensurePermission();
$file = $request->file('file');
// Basic extension guard (defense in depth vs only MIME detection)
if (strtolower($file->getClientOriginalExtension()) !== 'docx') {
return redirect()->back()->withErrors(['file' => 'Datoteka mora biti DOCX.']);
}
$slug = Str::slug($request->slug);
// Determine next version if slug exists
$latest = DocumentTemplate::where('slug', $slug)->orderByDesc('version')->first();
$nextVersion = $latest ? ($latest->version + 1) : 1;
$hash = hash_file('sha256', $file->getRealPath());
$path = $file->store("document-templates/{$slug}/v{$nextVersion}", 'public');
// Scan tokens from uploaded DOCX (best effort)
$tokens = [];
try {
/** @var TokenScanner $scanner */
$scanner = app(TokenScanner::class);
$zip = new \ZipArchive;
$tmp = tempnam(sys_get_temp_dir(), 'tmpl');
copy($file->getRealPath(), $tmp);
if ($zip->open($tmp) === true) {
$xml = $zip->getFromName('word/document.xml');
if ($xml !== false) {
$tokens = $scanner->scan($xml);
}
$zip->close();
}
} catch (\Throwable $e) {
// swallow scanning errors
}
// (Future) Could refine allowed columns automatically based on tokens
$entities = ['contract', 'client_case', 'client', 'person', 'account'];
$columns = [
'contract' => ['reference', 'start_date', 'end_date', 'description'],
'client_case' => ['client_ref'],
'client' => [],
'person' => ['full_name', 'first_name', 'last_name', 'nu'],
// Add common account attributes; whitelist may further refine
'account' => ['reference', 'initial_amount', 'balance_amount', 'promise_date'],
];
$payload = [
'name' => $request->name,
'slug' => $slug,
'custom_name' => $request->custom_name,
'description' => $request->description,
'core_entity' => 'contract',
'entities' => $entities,
'columns' => $columns,
'version' => $nextVersion,
'engine' => 'tokens',
'file_path' => $path,
'file_hash' => $hash,
'file_size' => $file->getSize(),
'mime_type' => $file->getMimeType(),
'active' => true,
'created_by' => $latest ? $latest->created_by : Auth::id(), // preserve original author for lineage if re-upload
'updated_by' => Auth::id(),
'formatting_options' => [
'number_decimals' => 2,
'decimal_separator' => ',',
'thousands_separator' => '.',
'currency_symbol' => '€',
'currency_position' => 'after',
'currency_space' => true,
],
];
if (Schema::hasColumn('document_templates', 'tokens')) {
$payload['tokens'] = $tokens;
}
$template = DocumentTemplate::create($payload);
return redirect()->back()->with('success', 'Predloga uspešno shranjena. (v'.$template->version.')')->with('template_id', $template->id);
}
private function ensurePermission(): void
{
if (Gate::denies('manage-document-templates') && Gate::denies('manage-settings')) {
abort(403);
}
}
}

View File

@ -24,7 +24,12 @@ public function resolve(array $tokens, DocumentTemplate $template, Contract $con
$configWhitelist = config('documents.whitelist', []); $configWhitelist = config('documents.whitelist', []);
// Merge preserving DB additions/overrides // Merge preserving DB additions/overrides
$globalWhitelist = array_replace($configWhitelist, $settingsWhitelist); $globalWhitelist = array_replace($configWhitelist, $settingsWhitelist);
$templateEntities = $template->entities ?: array_keys($globalWhitelist); // Always treat globally whitelisted entities as available, even if legacy template does not list them
if ($template->entities && is_array($template->entities)) {
$templateEntities = array_values(array_unique(array_merge($template->entities, array_keys($globalWhitelist))));
} else {
$templateEntities = array_keys($globalWhitelist);
}
foreach ($tokens as $token) { foreach ($tokens as $token) {
[$entity,$attr] = explode('.', $token, 2); [$entity,$attr] = explode('.', $token, 2);
if ($entity === 'generation') { if ($entity === 'generation') {
@ -80,6 +85,7 @@ private function entityAttribute(string $entity, string $attr, Contract $contrac
return (string) $person->{$attr}; return (string) $person->{$attr};
case 'account': case 'account':
$account = optional($contract->account); $account = optional($contract->account);
return (string) $account->{$attr}; return (string) $account->{$attr};
default: default:
return ''; return '';