mandant-crm/backend/tests/Feature/Modules/Import/ClientImportTest.php

223 lines
7.8 KiB
PHP
Raw Normal View History

<?php
use App\Models\User;
use App\Modules\Client\Models\Client;
use App\Modules\CustomField\Models\CustomFieldDefinition;
use App\Modules\CustomField\Models\CustomFieldValue;
use Database\Seeders\CustomFieldDefinitionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->seed(CustomFieldDefinitionSeeder::class);
});
/**
* @param array<int, array<int, string>> $rows
*/
function importCsv(array $rows): string
{
return implode("\n", array_map(static fn (array $r): string => implode(';', $r), $rows))."\n";
}
function importFile(string $contents, string $name = 'import.csv'): UploadedFile
{
return UploadedFile::fake()->createWithContent($name, $contents);
}
function postImport($test, UploadedFile $file, User $user)
{
return $test->actingAs($user)->post('/api/clients/import', ['file' => $file], ['Accept' => 'application/json']);
}
const IMPORT_HEADER = [
'Mandantennr.', 'Mandant', 'Anschrift 1', 'Anschrift 2',
'Telefon', 'E-Mail', 'Ansprechpartner 1', 'Ueberschreiben',
];
it('creates a new client with mapped custom fields and writes no history', function () {
$admin = User::factory()->create(['role' => 'admin']);
$csv = importCsv([
IMPORT_HEADER,
['1001', 'Mustermann GmbH', 'Hauptstr. 1', '12345 Musterstadt', '0123456', 'a@b.de', 'Herr Müller', ''],
]);
postImport($this, importFile($csv), $admin)
->assertStatus(200)
->assertJsonPath('data.created', 1)
->assertJsonPath('data.skipped', 0)
->assertJsonPath('data.updated', 0);
$client = Client::where('client_number', '1001')->firstOrFail();
expect($client->name)->toBe('Mustermann GmbH');
$tel = CustomFieldDefinition::where('key', 'telefonnummer')->firstOrFail();
$address = CustomFieldDefinition::where('key', 'anschrift')->firstOrFail();
$this->assertDatabaseHas('custom_field_values', [
'client_id' => $client->id,
'custom_field_definition_id' => $tel->id,
'value' => '0123456',
]);
$this->assertDatabaseHas('custom_field_values', [
'client_id' => $client->id,
'custom_field_definition_id' => $address->id,
'value' => "Hauptstr. 1\n12345 Musterstadt",
]);
// New clients are imported silently.
$this->assertDatabaseCount('history_entries', 0);
});
it('skips an existing client when the overwrite flag is absent', function () {
$admin = User::factory()->create(['role' => 'admin']);
Client::factory()->create(['client_number' => '2002', 'name' => 'Bestand AG']);
$csv = importCsv([
IMPORT_HEADER,
['2002', 'Geändert AG', '', '', '0999', 'neu@x.de', 'Herr Neu', ''],
]);
postImport($this, importFile($csv), $admin)
->assertStatus(200)
->assertJsonPath('data.skipped', 1)
->assertJsonPath('data.created', 0)
->assertJsonPath('data.updated', 0);
expect(Client::where('client_number', '2002')->first()->name)->toBe('Bestand AG');
$this->assertDatabaseCount('custom_field_values', 0);
$this->assertDatabaseCount('history_entries', 0);
});
it('overwrites custom fields of an existing client and audits each change', function () {
$admin = User::factory()->create(['role' => 'admin']);
$client = Client::factory()->create(['client_number' => '3003', 'name' => 'Original Name']);
$csv = importCsv([
IMPORT_HEADER,
['3003', 'Neuer Name', 'Weg 5', '', '0555', 'mail@z.de', 'Frau Schmidt', 'Ja'],
]);
postImport($this, importFile($csv), $admin)
->assertStatus(200)
->assertJsonPath('data.updated', 1)
->assertJsonPath('data.created', 0)
->assertJsonPath('data.skipped', 0);
// Name / number are never touched on overwrite — custom fields only.
expect($client->fresh()->name)->toBe('Original Name');
// Four non-blank fields → four field-change audit rows.
$this->assertDatabaseCount('history_entries', 4);
$importUser = User::where('name', 'Mandanten-Import')->firstOrFail();
$this->assertDatabaseHas('history_entries', [
'client_id' => $client->id,
'type' => 'field_change',
'author_id' => $importUser->id,
]);
// Author renders as "Mandanten-Import" through the history API.
$this->actingAs($admin)->getJson("/api/clients/{$client->id}/history")
->assertStatus(200)
->assertJsonPath('data.0.author_name', 'Mandanten-Import')
->assertJsonPath('data.0.type_name', 'Stammdatenänderung');
});
it('preserves existing values when an overwrite row has blanks', function () {
$admin = User::factory()->create(['role' => 'admin']);
$client = Client::factory()->create(['client_number' => '4004', 'name' => 'Firma']);
$tel = CustomFieldDefinition::where('key', 'telefonnummer')->firstOrFail();
CustomFieldValue::create([
'client_id' => $client->id,
'custom_field_definition_id' => $tel->id,
'value' => 'ALT-TELEFON',
'author_id' => $admin->id,
]);
// Telefon column blank → must not wipe the existing value.
$csv = importCsv([
IMPORT_HEADER,
['4004', 'Firma', '', '', '', 'only@mail.de', '', 'ja'],
]);
postImport($this, importFile($csv), $admin)
->assertStatus(200)
->assertJsonPath('data.updated', 1);
$this->assertDatabaseHas('custom_field_values', [
'client_id' => $client->id,
'custom_field_definition_id' => $tel->id,
'value' => 'ALT-TELEFON',
]);
});
it('strips Excel leading apostrophes and decodes ISO-8859-1 umlauts', function () {
$admin = User::factory()->create(['role' => 'admin']);
$csv = importCsv([
IMPORT_HEADER,
['5005', 'Bäcker Schröder', 'Straße 1', '', "'0301234", 'm@n.de', 'Herr Groß', ''],
]);
$latin1 = mb_convert_encoding($csv, 'ISO-8859-1', 'UTF-8');
postImport($this, importFile($latin1), $admin)
->assertStatus(200)
->assertJsonPath('data.created', 1);
$client = Client::where('client_number', '5005')->firstOrFail();
expect($client->name)->toBe('Bäcker Schröder');
$tel = CustomFieldDefinition::where('key', 'telefonnummer')->firstOrFail();
$this->assertDatabaseHas('custom_field_values', [
'client_id' => $client->id,
'custom_field_definition_id' => $tel->id,
'value' => '0301234', // leading apostrophe stripped
]);
});
it('handles ragged rows shorter than the header', function () {
$admin = User::factory()->create(['role' => 'admin']);
// Row stops after "Mandant" — every mapped column beyond it is missing.
$csv = "Mandantennr.;Mandant;Anschrift 1;Telefon;E-Mail;Ansprechpartner 1;Ueberschreiben\n6006;Kurz GmbH\n";
postImport($this, importFile($csv), $admin)
->assertStatus(200)
->assertJsonPath('data.created', 1);
expect(Client::where('client_number', '6006')->first()->name)->toBe('Kurz GmbH');
});
it('forbids non-admin users', function () {
$staff = User::factory()->create(['role' => 'staff']);
$csv = importCsv([IMPORT_HEADER, ['7007', 'X', '', '', '', '', '', '']]);
postImport($this, importFile($csv), $staff)->assertStatus(403);
$this->assertDatabaseCount('clients', 0);
});
it('rejects a non-csv upload', function () {
$admin = User::factory()->create(['role' => 'admin']);
$file = UploadedFile::fake()->createWithContent('data.pdf', 'irrelevant');
postImport($this, $file, $admin)->assertStatus(422);
});
it('reports an error row for a missing client number', function () {
$admin = User::factory()->create(['role' => 'admin']);
$csv = importCsv([
IMPORT_HEADER,
['', 'Ohne Nummer', '', '', '', '', '', ''],
]);
postImport($this, importFile($csv), $admin)
->assertStatus(200)
->assertJsonPath('data.created', 0)
->assertJsonCount(1, 'data.errors')
->assertJsonPath('data.errors.0.row', 2);
});