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.
162 lines
6.2 KiB
PHP
162 lines
6.2 KiB
PHP
<?php
|
|
|
|
use App\Models\User;
|
|
use App\Modules\Client\Models\Client;
|
|
use App\Modules\History\Models\HistoryEntry;
|
|
use App\Modules\History\Models\HistoryEntryType;
|
|
use App\Modules\History\Models\HistoryType;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('staff can list history entries for a client', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
HistoryEntry::factory(3)->create(['client_id' => $client->id, 'author_id' => $user->id]);
|
|
|
|
$response = $this->actingAs($user)->getJson("/api/clients/{$client->id}/history");
|
|
|
|
$response->assertStatus(200)->assertJsonCount(3, 'data');
|
|
});
|
|
|
|
it('staff can add a history entry', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
$type = HistoryType::factory()->create(['name' => 'Bemerkung']);
|
|
|
|
$response = $this->actingAs($user)->postJson("/api/clients/{$client->id}/history", [
|
|
'history_entry_type_id' => $type->id,
|
|
'body' => 'Rückruf vereinbart.',
|
|
]);
|
|
|
|
$response->assertStatus(201)
|
|
->assertJsonPath('data.history_entry_type_id', $type->id)
|
|
->assertJsonPath('data.type_name', 'Bemerkung')
|
|
->assertJsonPath('data.requires_datetime', false)
|
|
->assertJsonPath('data.body', 'Rückruf vereinbart.')
|
|
->assertJsonPath('data.author_id', $user->id);
|
|
|
|
$this->assertDatabaseHas('history_entries', [
|
|
'client_id' => $client->id,
|
|
'history_entry_type_id' => $type->id,
|
|
]);
|
|
});
|
|
|
|
it('stores and echoes back the occurred_at datetime unchanged', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
$type = HistoryType::factory()->requiresDatetime()->create(['name' => 'Telefonat']);
|
|
|
|
$response = $this->actingAs($user)->postJson("/api/clients/{$client->id}/history", [
|
|
'history_entry_type_id' => $type->id,
|
|
'body' => 'Telefonat geführt.',
|
|
'occurred_at' => '2026-06-25T14:30:00',
|
|
]);
|
|
|
|
$response->assertStatus(201)
|
|
->assertJsonPath('data.type_name', 'Telefonat')
|
|
->assertJsonPath('data.requires_datetime', true)
|
|
->assertJsonPath('data.occurred_at', '2026-06-25T14:30:00');
|
|
});
|
|
|
|
it('requires occurred_at when the type requires a datetime', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
$type = HistoryType::factory()->requiresDatetime()->create();
|
|
|
|
$this->actingAs($user)->postJson("/api/clients/{$client->id}/history", [
|
|
'history_entry_type_id' => $type->id,
|
|
'body' => 'Ohne Zeit.',
|
|
])->assertStatus(422)->assertJsonValidationErrors('occurred_at');
|
|
});
|
|
|
|
it('rejects an invalid history_entry_type_id', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
|
|
$this->actingAs($user)->postJson("/api/clients/{$client->id}/history", [
|
|
'history_entry_type_id' => 99999,
|
|
'body' => 'test',
|
|
])->assertStatus(422)->assertJsonValidationErrors('history_entry_type_id');
|
|
});
|
|
|
|
it('lets the author edit their own entry', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
$entry = HistoryEntry::factory()->create([
|
|
'client_id' => $client->id,
|
|
'author_id' => $user->id,
|
|
'body' => 'Original',
|
|
]);
|
|
|
|
$response = $this->actingAs($user)->putJson("/api/history/{$entry->id}", [
|
|
'body' => 'Updated body',
|
|
]);
|
|
|
|
$response->assertStatus(200)->assertJsonPath('data.body', 'Updated body');
|
|
$this->assertDatabaseHas('history_entries', ['id' => $entry->id, 'updated_by' => $user->id]);
|
|
});
|
|
|
|
it('forbids a non-author from editing an entry', function () {
|
|
$author = User::factory()->create(['role' => 'staff']);
|
|
$other = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
$entry = HistoryEntry::factory()->create([
|
|
'client_id' => $client->id,
|
|
'author_id' => $author->id,
|
|
'body' => 'Original',
|
|
]);
|
|
|
|
$this->actingAs($other)->putJson("/api/history/{$entry->id}", [
|
|
'body' => 'tampered',
|
|
])->assertStatus(403);
|
|
});
|
|
|
|
it('nobody can edit a field_change audit entry', function () {
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$client = Client::factory()->create();
|
|
$entry = HistoryEntry::factory()->create([
|
|
'client_id' => $client->id,
|
|
'author_id' => $admin->id,
|
|
'history_entry_type_id' => null,
|
|
'type' => HistoryEntryType::FieldChange->value,
|
|
]);
|
|
|
|
$this->actingAs($admin)->putJson("/api/history/{$entry->id}", ['body' => 'tampered'])->assertStatus(403);
|
|
});
|
|
|
|
it('renders a field_change entry as Stammdatenänderung', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
HistoryEntry::factory()->create([
|
|
'client_id' => $client->id,
|
|
'author_id' => $user->id,
|
|
'history_entry_type_id' => null,
|
|
'type' => HistoryEntryType::FieldChange->value,
|
|
]);
|
|
|
|
$this->actingAs($user)->getJson("/api/clients/{$client->id}/history")
|
|
->assertStatus(200)
|
|
->assertJsonPath('data.0.is_field_change', true)
|
|
->assertJsonPath('data.0.type_name', 'Stammdatenänderung');
|
|
});
|
|
|
|
it('admin can soft-delete a history entry', function () {
|
|
$admin = User::factory()->create(['role' => 'admin']);
|
|
$client = Client::factory()->create();
|
|
$entry = HistoryEntry::factory()->create(['client_id' => $client->id, 'author_id' => $admin->id]);
|
|
|
|
$response = $this->actingAs($admin)->deleteJson("/api/history/{$entry->id}");
|
|
|
|
$response->assertStatus(204);
|
|
$this->assertSoftDeleted('history_entries', ['id' => $entry->id]);
|
|
});
|
|
|
|
it('staff cannot delete a history entry', function () {
|
|
$user = User::factory()->create(['role' => 'staff']);
|
|
$client = Client::factory()->create();
|
|
$entry = HistoryEntry::factory()->create(['client_id' => $client->id, 'author_id' => $user->id]);
|
|
|
|
$this->actingAs($user)->deleteJson("/api/history/{$entry->id}")->assertStatus(403);
|
|
$this->assertDatabaseHas('history_entries', ['id' => $entry->id, 'deleted_at' => null]);
|
|
});
|