work in progress: predefinedmutations
This commit is contained in:
parent
806d17842f
commit
89a48fae16
@ -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.
|
||||
|
||||
@ -69,6 +69,7 @@ class LedgerController extends Controller
|
||||
|
||||
$ledger->load([
|
||||
'media',
|
||||
'predefinedMutations',
|
||||
'mutations' => function ($query) {
|
||||
$query->latest();
|
||||
},
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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'],
|
||||
];
|
||||
|
||||
@ -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() {
|
||||
<div class="c-add-mutation-form">
|
||||
<h4 class="c-add-mutation-form__title">Add Mutation</h4>
|
||||
<form @submit.prevent="submit" class="c-add-mutation-form__form">
|
||||
|
||||
<!-- Predefined Templates Selection -->
|
||||
<div v-if="predefinedMutations && predefinedMutations.length > 0" class="c-add-mutation-form__field">
|
||||
<label for="predefined_mutation" class="c-add-mutation-form__label"
|
||||
>Apply Predefined Template</label
|
||||
>
|
||||
<select
|
||||
id="predefined_mutation"
|
||||
class="c-add-mutation-form__select"
|
||||
@change="selectPredefinedMutation"
|
||||
>
|
||||
<option value="">-- Choose a predefined template (optional) --</option>
|
||||
<option
|
||||
v-for="item in predefinedMutations"
|
||||
:key="item.id"
|
||||
:value="item.id"
|
||||
>
|
||||
{{ item.name }} ({{ item.amount >= 0 ? '+' : '' }}{{ item.amount }} points)
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="c-add-mutation-form__field">
|
||||
<label for="amount" class="c-add-mutation-form__label"
|
||||
>Amount</label
|
||||
@ -151,6 +202,10 @@ function submit() {
|
||||
@apply block text-sm font-medium text-gray-700 dark:text-gray-300;
|
||||
}
|
||||
|
||||
.c-add-mutation-form__select {
|
||||
@apply mt-1 block w-full rounded-md border border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
|
||||
}
|
||||
|
||||
.c-add-mutation-form__input {
|
||||
@apply mt-1 block w-full rounded-md border border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600;
|
||||
}
|
||||
|
||||
@ -45,6 +45,12 @@ const props = defineProps<{
|
||||
alignment: string;
|
||||
status: string;
|
||||
media?: Array<{ id: number; url: string; mime_type: string }>;
|
||||
predefined_mutations?: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
amount: number;
|
||||
}>;
|
||||
mutations: Array<{
|
||||
id: number;
|
||||
user_id: number;
|
||||
@ -262,7 +268,7 @@ function isOwnerUser(userId: number): boolean {
|
||||
</div>
|
||||
|
||||
<!-- Add Mutation Form Component -->
|
||||
<AddMutationForm :dynamic-id="dynamic.id" :ledger-id="ledger.id" />
|
||||
<AddMutationForm :dynamic-id="dynamic.id" :ledger-id="ledger.id" :predefined-mutations="ledger.predefined_mutations" />
|
||||
|
||||
<!-- Mutation List Component -->
|
||||
<MutationList
|
||||
|
||||
@ -172,3 +172,39 @@ test('creating a mutation with less than -1000 points fails validation', functio
|
||||
$response->assertSessionHasErrors(['amount']);
|
||||
expect(Mutation::where('description', 'Abusive negative point demerit')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('participant can choose and request a predefined mutation', function () {
|
||||
$owner = User::factory()->create();
|
||||
$participant = User::factory()->create();
|
||||
$dynamic = Dynamic::factory()->create();
|
||||
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
|
||||
$dynamic->participants()->attach($participant->id, ['role' => 'participant']);
|
||||
$ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id, 'score' => 100]);
|
||||
|
||||
$predefined = \App\Models\PredefinedMutation::create([
|
||||
'ledger_id' => $ledger->id,
|
||||
'name' => 'Wash the Motorbunny',
|
||||
'amount' => 50,
|
||||
'description' => 'Must be fully cleaned and dried.',
|
||||
]);
|
||||
|
||||
$this->actingAs($participant);
|
||||
|
||||
$response = $this->post(route('dynamics.ledgers.mutations.store', [$dynamic, $ledger]), [
|
||||
'amount' => 50,
|
||||
'description' => 'Completed Wash the Motorbunny template chore.',
|
||||
'predefined_mutation_id' => $predefined->uuid,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('dynamics.ledgers.show', [$dynamic, $ledger]));
|
||||
|
||||
$mutation = Mutation::firstWhere('description', 'Completed Wash the Motorbunny template chore.');
|
||||
|
||||
expect($mutation)->not->toBeNull();
|
||||
expect($mutation->status)->toBe('pending');
|
||||
expect($mutation->predefined_mutation_id)->toBe($predefined->id);
|
||||
|
||||
// Score should NOT be updated since it is pending!
|
||||
$ledger->refresh();
|
||||
expect($ledger->score)->toBe(100);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user