diff --git a/GEMINI.md b/GEMINI.md index 82725e0..37dc989 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -30,3 +30,12 @@ Welcome to the Ledgerrz codebase! This file defines the persistent guidelines, a }) }); ``` + +--- + +## 3. Advanced Browser Testing & Architectural Guidelines (Laravel Dusk) +* **Dusk Database Locks on SQLite:** Never use the `DatabaseTransactions` trait in Laravel Dusk tests if the project is backed by a SQLite database file. Since Dusk runs in concurrent, separate processes (the CLI runner and the web server), an active uncommitted database transaction in the CLI will hold a SQLite write lock. This causes the web server to fail with `SQLSTATE[HY000]: General error: 5 database is locked`. Instead, dynamically create and clean up models during test execution or use sequential DB seeds. +* **Inertia/Vue SPA Testing:** Browser tests targeting asynchronously rendered Single Page Applications must **always** use explicit `waitForText()` or `waitFor()` selectors before executing assertions. Direct assertions like `assertSee()` are immediate and will fail if the JavaScript has not finished compiling and mounting the DOM. +* **Synchronous Queue for WebSockets:** Running real-time WebSocket / Echo Dusk tests locally requires setting `QUEUE_CONNECTION=sync` and `BROADCAST_CONNECTION=reverb` in your `.env` file. This ensures broadcast jobs execute immediately on the web server rather than buffering in the database (which fails without a running background queue worker). +* **Automatic ID-to-UUID Serialization:** All models carrying a `uuid` column (such as `User`, `Dynamic`, `Ledger`, `Mutation`, and `PredefinedMutation`) must use the `App\Concerns\SerializesIdToUuid` trait. This guarantees that any raw model or relationship array/JSON conversion automatically replaces the integer `id` with the secure `uuid` under the `'id'` key. +* **UUID Controller Inputs:** When the frontend submits relationship parameters (such as `predefined_mutation_id` in requests), it only knows and passes the secure UUID. The receiving backend controller must explicitly resolve this UUID to its internal database integer ID before inserting the record (e.g. `PredefinedMutation::where('uuid', $uuid)->value('id')`), maintaining column type safety and foreign key constraints without exposing integer IDs to the client. diff --git a/app/Http/Controllers/LedgerController.php b/app/Http/Controllers/LedgerController.php index 459cb67..011c088 100644 --- a/app/Http/Controllers/LedgerController.php +++ b/app/Http/Controllers/LedgerController.php @@ -69,6 +69,7 @@ class LedgerController extends Controller $ledger->load([ 'media', + 'predefinedMutations', 'mutations' => function ($query) { $query->latest(); }, diff --git a/app/Http/Controllers/MutationController.php b/app/Http/Controllers/MutationController.php index 715a1bf..afce75e 100644 --- a/app/Http/Controllers/MutationController.php +++ b/app/Http/Controllers/MutationController.php @@ -44,8 +44,14 @@ class MutationController extends Controller $status = $request->user()->can('update', $ledger) ? 'approved' : 'pending'; $mutation = DB::transaction(function () use ($request, $ledger, $status) { + $predefinedId = null; + if ($request->filled('predefined_mutation_id')) { + $predefinedId = \App\Models\PredefinedMutation::where('uuid', $request->input('predefined_mutation_id'))->value('id'); + } + $mutation = $ledger->mutations()->create([ - ...$request->except(['media', 'type', 'status']), + ...$request->except(['media', 'type', 'status', 'predefined_mutation_id']), + 'predefined_mutation_id' => $predefinedId, 'user_id' => $request->user()->id, 'type' => $request->input('type', $request->input('amount') >= 0 ? 'addition' : 'subtraction'), 'status' => $status, diff --git a/app/Http/Requests/StoreMutationRequest.php b/app/Http/Requests/StoreMutationRequest.php index 8a40e8c..eb6e286 100644 --- a/app/Http/Requests/StoreMutationRequest.php +++ b/app/Http/Requests/StoreMutationRequest.php @@ -29,6 +29,7 @@ class StoreMutationRequest extends FormRequest 'description' => ['required', 'string'], 'type' => ['nullable', 'string'], 'status' => ['nullable', 'string'], + 'predefined_mutation_id' => ['nullable', 'exists:predefined_mutations,uuid'], 'media' => ['nullable', 'array'], 'media.*' => ['file', 'mimes:jpg,jpeg,png,gif,mp4,mov,avi,webm', 'max:20480'], ]; diff --git a/resources/js/components/AddMutationForm.vue b/resources/js/components/AddMutationForm.vue index 22185ba..b28af2b 100644 --- a/resources/js/components/AddMutationForm.vue +++ b/resources/js/components/AddMutationForm.vue @@ -2,17 +2,46 @@ import { useForm } from '@inertiajs/vue3'; import { route } from 'ziggy-js'; -const props = defineProps<{ - dynamicId: string; - ledgerId: string; -}>(); +const props = withDefaults( + defineProps<{ + dynamicId: string; + ledgerId: string; + predefinedMutations?: Array<{ + id: string; + name: string; + description: string | null; + amount: number; + }>; + }>(), + { + predefinedMutations: () => [], + } +); const form = useForm({ amount: 0, description: '', + predefined_mutation_id: null as string | null, media: [] as File[], }); +function selectPredefinedMutation(event: Event) { + const select = event.target as HTMLSelectElement; + const selectedId = select.value; + if (!selectedId) { + form.predefined_mutation_id = null; + form.amount = 0; + form.description = ''; + return; + } + const mutation = props.predefinedMutations.find(m => m.id === selectedId); + if (mutation) { + form.predefined_mutation_id = mutation.id; + form.amount = mutation.amount; + form.description = mutation.description || mutation.name; + } +} + function handleMutationFileChange(event: Event) { const files = (event.target as HTMLInputElement).files; @@ -44,6 +73,28 @@ function submit() {