Mirrors the frontend catch-up in 570f17e: a substantial amount of
finished-but-never-committed backend work had accumulated locally.
- New CustomField module: admin-managed field definitions per client,
reorderable, typed values stored per client (backing the frontend's
ClientCustomFieldCard + settings/fields page)
- History entries switch from a hardcoded type enum to a dynamic,
admin-managed HistoryType module (reorderable, backing
settings/history-types)
- New Import module: CSV client import (backing settings/import)
- Clients table gains soft deletes (deleted_at)
- Supporting factories, seeders, policies, and feature tests for all
of the above
All 63 backend tests pass locally before this commit.
158 lines
6.1 KiB
PHP
158 lines
6.1 KiB
PHP
<?php
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Client\Models\Client;
|
|
use App\Modules\CustomField\Models\CustomFieldDefinition;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('lets any authenticated user list field definitions', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
CustomFieldDefinition::factory(2)->create();
|
|
|
|
$this->actingAs($user)->getJson('/api/custom-fields')
|
|
->assertStatus(200)
|
|
->assertJsonCount(2, 'data');
|
|
});
|
|
|
|
it('lets an admin create a field definition and derives a unique key', function () {
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
|
|
$this->actingAs($admin)->postJson('/api/custom-fields', [
|
|
'label' => 'Beratungsschwerpunkt',
|
|
'type' => 'select',
|
|
'options' => ['Steuerrecht', 'Erbrecht'],
|
|
])->assertStatus(201)->assertJsonPath('data.key', 'beratungsschwerpunkt');
|
|
|
|
$this->assertDatabaseHas('custom_field_definitions', ['label' => 'Beratungsschwerpunkt']);
|
|
});
|
|
|
|
it('appends new field definitions to the end of the list', function () {
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
CustomFieldDefinition::factory()->create(['sort_order' => 2]);
|
|
|
|
$this->actingAs($admin)->postJson('/api/custom-fields', [
|
|
'label' => 'Zuletzt',
|
|
'type' => 'text',
|
|
])->assertStatus(201)->assertJsonPath('data.sort_order', 3);
|
|
});
|
|
|
|
it('lets an admin reorder field definitions', function () {
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$a = CustomFieldDefinition::factory()->create(['sort_order' => 0]);
|
|
$b = CustomFieldDefinition::factory()->create(['sort_order' => 1]);
|
|
$c = CustomFieldDefinition::factory()->create(['sort_order' => 2]);
|
|
|
|
$this->actingAs($admin)->postJson('/api/custom-fields/reorder', [
|
|
'ids' => [$c->id, $a->id, $b->id],
|
|
])->assertStatus(200)
|
|
->assertJsonPath('data.0.id', $c->id)
|
|
->assertJsonPath('data.1.id', $a->id)
|
|
->assertJsonPath('data.2.id', $b->id);
|
|
|
|
$this->assertDatabaseHas('custom_field_definitions', ['id' => $c->id, 'sort_order' => 0]);
|
|
$this->assertDatabaseHas('custom_field_definitions', ['id' => $a->id, 'sort_order' => 1]);
|
|
$this->assertDatabaseHas('custom_field_definitions', ['id' => $b->id, 'sort_order' => 2]);
|
|
});
|
|
|
|
it('forbids staff from reordering field definitions', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$definition = CustomFieldDefinition::factory()->create();
|
|
|
|
$this->actingAs($user)->postJson('/api/custom-fields/reorder', [
|
|
'ids' => [$definition->id],
|
|
])->assertStatus(403);
|
|
});
|
|
|
|
it('forbids staff from creating field definitions', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
|
|
$this->actingAs($user)->postJson('/api/custom-fields', [
|
|
'label' => 'Heimlich',
|
|
'type' => 'text',
|
|
])->assertStatus(403);
|
|
});
|
|
|
|
it('forbids staff from deleting field definitions', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$definition = CustomFieldDefinition::factory()->create();
|
|
|
|
$this->actingAs($user)->deleteJson("/api/custom-fields/{$definition->id}")->assertStatus(403);
|
|
});
|
|
|
|
it('requires options when the type is select', function () {
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
|
|
$this->actingAs($admin)->postJson('/api/custom-fields', [
|
|
'label' => 'Ohne Optionen',
|
|
'type' => 'select',
|
|
])->assertStatus(422);
|
|
});
|
|
|
|
it('records a Verlauf entry when a field value changes', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
$definition = CustomFieldDefinition::factory()->create(['label' => 'Beratungsschwerpunkt']);
|
|
|
|
$this->actingAs($user)->putJson("/api/clients/{$client->id}/custom-fields/{$definition->id}", [
|
|
'value' => 'Steuerrecht',
|
|
])->assertStatus(200);
|
|
|
|
$this->assertDatabaseHas('custom_field_values', [
|
|
'client_id' => $client->id,
|
|
'custom_field_definition_id' => $definition->id,
|
|
'value' => 'Steuerrecht',
|
|
'author_id' => $user->id,
|
|
]);
|
|
|
|
$this->assertDatabaseHas('history_entries', [
|
|
'client_id' => $client->id,
|
|
'type' => 'field_change',
|
|
'custom_field_definition_id' => $definition->id,
|
|
'author_id' => $user->id,
|
|
]);
|
|
});
|
|
|
|
it('does not record a Verlauf entry when the value is unchanged', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
$definition = CustomFieldDefinition::factory()->create();
|
|
|
|
$url = "/api/clients/{$client->id}/custom-fields/{$definition->id}";
|
|
$this->actingAs($user)->putJson($url, ['value' => 'Hallo'])->assertStatus(200);
|
|
$this->actingAs($user)->putJson($url, ['value' => 'Hallo'])->assertStatus(200);
|
|
|
|
expect(\App\Modules\History\Models\HistoryEntry::where('client_id', $client->id)->count())->toBe(1);
|
|
});
|
|
|
|
it('validates select values against the defined options', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
$definition = CustomFieldDefinition::factory()->select(['Steuerrecht', 'Erbrecht'])->create();
|
|
|
|
$this->actingAs($user)->putJson("/api/clients/{$client->id}/custom-fields/{$definition->id}", [
|
|
'value' => 'Strafrecht',
|
|
])->assertStatus(422);
|
|
});
|
|
|
|
it('validates number values', function () {
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$client = Client::factory()->create();
|
|
$definition = CustomFieldDefinition::factory()->create(['type' => 'number']);
|
|
|
|
$this->actingAs($admin)->putJson("/api/clients/{$client->id}/custom-fields/{$definition->id}", [
|
|
'value' => 'not-a-number',
|
|
])->assertStatus(422);
|
|
});
|
|
|
|
it('embeds custom fields on the client detail response', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
CustomFieldDefinition::factory()->create(['label' => 'Mandant seit', 'sort_order' => 1]);
|
|
|
|
$this->actingAs($user)->getJson("/api/clients/{$client->id}")
|
|
->assertStatus(200)
|
|
->assertJsonPath('data.custom_fields.0.label', 'Mandant seit')
|
|
->assertJsonPath('data.custom_fields.0.value', null);
|
|
});
|