223 lines
7.8 KiB
PHP
223 lines
7.8 KiB
PHP
|
|
<?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);
|
||
|
|
});
|