initial commit
This commit is contained in:
166
tests/Feature/ContactControllerTest.php
Normal file
166
tests/Feature/ContactControllerTest.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Contact;
|
||||
use Illuminate\Foundation\Http\Middleware\ValidateCsrfToken;
|
||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ContactControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->withoutMiddleware([
|
||||
VerifyCsrfToken::class,
|
||||
ValidateCsrfToken::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testIndexDisplaysContacts(): void
|
||||
{
|
||||
$contacts = Contact::factory()->count(2)->create();
|
||||
|
||||
$response = $this->get(route('contacts.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee($contacts[0]->email);
|
||||
$response->assertSee($contacts[1]->email);
|
||||
}
|
||||
|
||||
public function testCreateDisplaysForm(): void
|
||||
{
|
||||
$response = $this->get(route('contacts.create'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('New Contact');
|
||||
}
|
||||
|
||||
public function testStoreCreatesContact(): void
|
||||
{
|
||||
$payload = [
|
||||
'first_name' => 'Jane',
|
||||
'last_name' => 'Doe',
|
||||
'email' => 'jane@example.com',
|
||||
];
|
||||
|
||||
$response = $this->post(route('contacts.store'), $payload);
|
||||
|
||||
$contact = Contact::query()->first();
|
||||
|
||||
$this->assertNotNull($contact);
|
||||
$this->assertSame('jane@example.com', $contact->email);
|
||||
|
||||
$response->assertRedirect(route('contacts.show', $contact));
|
||||
$this->assertDatabaseHas('contacts', ['email' => 'jane@example.com']);
|
||||
}
|
||||
|
||||
public function testStoreRequiresEmail(): void
|
||||
{
|
||||
$response = $this->post(route('contacts.store'), [
|
||||
'first_name' => 'Jane',
|
||||
'last_name' => 'Doe',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
$this->assertDatabaseCount('contacts', 0);
|
||||
}
|
||||
|
||||
public function testShowDisplaysContact(): void
|
||||
{
|
||||
$contact = Contact::factory()->create([
|
||||
'first_name' => 'Alex',
|
||||
'last_name' => 'Novak',
|
||||
'email' => 'alex@example.com',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('contacts.show', $contact));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Alex');
|
||||
$response->assertSee('Novak');
|
||||
$response->assertSee('alex@example.com');
|
||||
}
|
||||
|
||||
public function testEditDisplaysContact(): void
|
||||
{
|
||||
$contact = Contact::factory()->create([
|
||||
'email' => 'editme@example.com',
|
||||
]);
|
||||
|
||||
$response = $this->get(route('contacts.edit', $contact));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Edit Contact');
|
||||
$response->assertSee('editme@example.com');
|
||||
}
|
||||
|
||||
public function testUpdatePersistsChanges(): void
|
||||
{
|
||||
$contact = Contact::factory()->create([
|
||||
'email' => 'old@example.com',
|
||||
]);
|
||||
|
||||
$response = $this->put(route('contacts.update', $contact), [
|
||||
'first_name' => 'Lukas',
|
||||
'last_name' => 'Svoboda',
|
||||
'email' => 'new@example.com',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('contacts.show', $contact));
|
||||
$this->assertDatabaseHas('contacts', [
|
||||
'id' => $contact->id,
|
||||
'first_name' => 'Lukas',
|
||||
'last_name' => 'Svoboda',
|
||||
'email' => 'new@example.com',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testUpdateRequiresUniqueEmail(): void
|
||||
{
|
||||
$first = Contact::factory()->create(['email' => 'first@example.com']);
|
||||
$second = Contact::factory()->create(['email' => 'second@example.com']);
|
||||
|
||||
$response = $this->put(route('contacts.update', $first), [
|
||||
'first_name' => $first->first_name,
|
||||
'last_name' => $first->last_name,
|
||||
'email' => $second->email,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
$this->assertDatabaseHas('contacts', ['email' => 'first@example.com']);
|
||||
}
|
||||
|
||||
public function testUpdateAllowsSameEmailForCurrentContact(): void
|
||||
{
|
||||
$contact = Contact::factory()->create([
|
||||
'email' => 'same@example.com',
|
||||
]);
|
||||
|
||||
$response = $this->put(route('contacts.update', $contact), [
|
||||
'first_name' => $contact->first_name,
|
||||
'last_name' => $contact->last_name,
|
||||
'email' => 'same@example.com',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('contacts.show', $contact));
|
||||
$this->assertDatabaseHas('contacts', ['email' => 'same@example.com']);
|
||||
}
|
||||
|
||||
public function testDestroyDeletesContact(): void
|
||||
{
|
||||
$contact = Contact::factory()->create();
|
||||
|
||||
$response = $this->delete(route('contacts.destroy', $contact));
|
||||
|
||||
$response->assertRedirect(route('contacts.index'));
|
||||
$this->assertDatabaseMissing('contacts', ['id' => $contact->id]);
|
||||
}
|
||||
}
|
||||
80
tests/Feature/ImportControllerTest.php
Normal file
80
tests/Feature/ImportControllerTest.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Jobs\ProcessImport;
|
||||
use App\Models\ContactImport;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ImportControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function testStoreQueuesImportAndRedirects(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
Queue::fake();
|
||||
$this->withoutMiddleware();
|
||||
|
||||
$file = UploadedFile::fake()->create(
|
||||
'contacts.xml',
|
||||
10,
|
||||
'text/xml',
|
||||
);
|
||||
|
||||
$response = $this->post(route('import.store'), [
|
||||
'file' => $file,
|
||||
]);
|
||||
|
||||
$response
|
||||
->assertRedirect(route('import.index'))
|
||||
->assertSessionHas('importUuid')
|
||||
->assertSessionHas('status', 'Import queued.');
|
||||
|
||||
Queue::assertPushed(ProcessImport::class);
|
||||
$this->assertDatabaseCount('contact_imports', 1);
|
||||
}
|
||||
|
||||
public function testShowReturnsImport(): void
|
||||
{
|
||||
$contactImport = ContactImport::factory()->create();
|
||||
|
||||
$response = $this->getJson(route('api.imports.show', $contactImport->id));
|
||||
|
||||
$response
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'id' => $contactImport->id,
|
||||
'state' => $contactImport->state->value,
|
||||
])
|
||||
->assertJsonStructure([
|
||||
'id',
|
||||
'queue_at',
|
||||
'started_at',
|
||||
'finished_at',
|
||||
'total_processed',
|
||||
'errors',
|
||||
'duplicates',
|
||||
'state',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testShowReturns404ForInvalidUuid(): void
|
||||
{
|
||||
$this->getJson('/api/imports/not-a-uuid')
|
||||
->assertNotFound();
|
||||
}
|
||||
|
||||
public function testShowReturns404ForMissingImport(): void
|
||||
{
|
||||
$this->getJson(route('api.imports.show', Uuid::uuid7()->toString()))
|
||||
->assertNotFound();
|
||||
}
|
||||
}
|
||||
12
tests/TestCase.php
Normal file
12
tests/TestCase.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
//
|
||||
}
|
||||
65
tests/Unit/CreateContactCommandTest.php
Normal file
65
tests/Unit/CreateContactCommandTest.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Data\Contact;
|
||||
use App\Data\Intent\Contact\CreateContactIntent;
|
||||
use App\Services\Command\CreateContactCommand;
|
||||
use App\Services\Mapper\ContactMapper;
|
||||
use App\Services\Storage\ContactStorageProvider;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CreateContactCommandTest extends TestCase
|
||||
{
|
||||
public function testExecuteCreatesContact(): void
|
||||
{
|
||||
$intent = new CreateContactIntent('jane@example.com', 'Jane', 'Doe');
|
||||
$contactMapper = new ContactMapper();
|
||||
|
||||
$contactStorageProvider = $this->createMock(ContactStorageProvider::class);
|
||||
$contactStorageProvider->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(static function (Contact $contact): bool {
|
||||
return $contact->email === 'jane@example.com'
|
||||
&& $contact->firstName === 'Jane'
|
||||
&& $contact->lastName === 'Doe';
|
||||
}))
|
||||
->willReturnCallback(static fn (Contact $contact): Contact => $contact);
|
||||
|
||||
$command = new CreateContactCommand($contactMapper, $contactStorageProvider);
|
||||
|
||||
$result = $command->execute($intent);
|
||||
|
||||
$this->assertInstanceOf(Contact::class, $result);
|
||||
$this->assertSame('jane@example.com', $result->email);
|
||||
}
|
||||
|
||||
public function testMultipleExecuteCountsFailsAndDuplicates(): void
|
||||
{
|
||||
$intentA = new CreateContactIntent('a@example.com', 'A', 'User');
|
||||
$intentB = new CreateContactIntent('invalid-email', 'B', 'User');
|
||||
$contactMapper = new ContactMapper();
|
||||
|
||||
$contactStorageProvider = $this->createMock(ContactStorageProvider::class);
|
||||
$contactStorageProvider->expects($this->once())
|
||||
->method('batchCreate')
|
||||
->with($this->callback(function (array $batch): bool {
|
||||
if (count($batch) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $batch[0] instanceof Contact;
|
||||
}))
|
||||
->willReturn(1);
|
||||
|
||||
$command = new CreateContactCommand($contactMapper, $contactStorageProvider);
|
||||
|
||||
$result = $command->multipleExecute([$intentA, $intentB]);
|
||||
|
||||
$this->assertSame(1, $result->fails);
|
||||
$this->assertSame(0, $result->duplicates);
|
||||
$this->assertSame(1, $result->success);
|
||||
}
|
||||
}
|
||||
293
tests/Unit/ProcessImportTest.php
Normal file
293
tests/Unit/ProcessImportTest.php
Normal file
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Data\Command\BatchResult;
|
||||
use App\Data\Intent\Contact\CreateContactIntent;
|
||||
use App\Jobs\ProcessImport;
|
||||
use App\Services\Command\CreateContactCommand;
|
||||
use App\Services\ProcessImportMonitor\ProcessImportState;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProcessImportTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
private array $tempFiles = [];
|
||||
|
||||
public function testHandleValidFile(): void
|
||||
{
|
||||
$xml = <<<'XML'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<data>
|
||||
<item>
|
||||
<email>god@foundation.edu</email>
|
||||
<first_name>Hari</first_name>
|
||||
<last_name>Seldon</last_name>
|
||||
</item>
|
||||
<item>
|
||||
<email>picard@federation.earth</email>
|
||||
<first_name>Jean-Luc</first_name>
|
||||
<last_name>Picard</last_name>
|
||||
</item>
|
||||
<item>
|
||||
<email>jackwithapostrophe@sgc.mil</email>
|
||||
<first_name>Jack</first_name>
|
||||
<last_name>O'Neill</last_name>
|
||||
</item>
|
||||
</data>
|
||||
XML;
|
||||
|
||||
$createContactCommand = $this->createCreateContactCommandMock(
|
||||
1,
|
||||
['god@foundation.edu', 'picard@federation.earth', 'jackwithapostrophe@sgc.mil'],
|
||||
new BatchResult(0, 0, 3),
|
||||
);
|
||||
|
||||
$processImportState = $this->createMock(ProcessImportState::class);
|
||||
$processImportState->expects($this->exactly(1))->method('start');
|
||||
$processImportState->expects($this->exactly(1))->method('finish');
|
||||
$processImportState->expects($this->exactly(1))->method('contactFail')->with(0);
|
||||
$processImportState->expects($this->exactly(1))->method('contactDuplicate')->with(0);
|
||||
$processImportState->expects($this->exactly(1))->method('contactSuccess')->with(3);
|
||||
$processImportState->method('uuid')->willReturn(Uuid::uuid7());
|
||||
|
||||
$job = new ProcessImport(
|
||||
$this->createTempFile($xml),
|
||||
$processImportState,
|
||||
);
|
||||
$job->handle($createContactCommand);
|
||||
}
|
||||
|
||||
public function testHandleMultipleTextNodes(): void
|
||||
{
|
||||
$xml = <<<'XML'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<data>
|
||||
<item>
|
||||
<email>god@<![CDATA[foundati]]>on.edu</email>
|
||||
<first_name>Hari</first_name>
|
||||
<last_name>Seldon</last_name>
|
||||
</item>
|
||||
</data>
|
||||
XML;
|
||||
|
||||
$createContactCommand = $this->createCreateContactCommandMock(
|
||||
1,
|
||||
['god@foundation.edu'],
|
||||
new BatchResult(0, 0, 1),
|
||||
);
|
||||
|
||||
$processImportState = $this->createMock(ProcessImportState::class);
|
||||
$processImportState->expects($this->exactly(1))->method('start');
|
||||
$processImportState->expects($this->exactly(1))->method('finish');
|
||||
$processImportState->expects($this->exactly(1))->method('contactFail')->with(0);
|
||||
$processImportState->expects($this->exactly(1))->method('contactDuplicate')->with(0);
|
||||
$processImportState->expects($this->exactly(1))->method('contactSuccess')->with(1);
|
||||
$processImportState->method('uuid')->willReturn(Uuid::uuid7());
|
||||
|
||||
$job = new ProcessImport(
|
||||
$this->createTempFile($xml),
|
||||
$processImportState,
|
||||
);
|
||||
$job->handle($createContactCommand);
|
||||
}
|
||||
|
||||
public function testHandleValidMultipleBatches(): void
|
||||
{
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?><data>';
|
||||
$emails = [];
|
||||
for ($i = 0; $i <= 5001; $i++) {
|
||||
$xml .= '<item>
|
||||
<email>god' . $i . '@foundation.edu</email>
|
||||
<first_name>Hari</first_name>
|
||||
<last_name>Seldon</last_name>
|
||||
</item>';
|
||||
$emails[] = 'god@foundation.edu' . $i;
|
||||
}
|
||||
$xml .= '</data>';
|
||||
|
||||
$createContactCommand = $this->createCreateContactCommandMock(
|
||||
2,
|
||||
[],
|
||||
[
|
||||
new BatchResult(0, 0, 5000),
|
||||
new BatchResult(0, 0, 1),
|
||||
],
|
||||
);
|
||||
$processImportState = $this->createMock(ProcessImportState::class);
|
||||
$processImportState->expects($this->exactly(1))->method('start');
|
||||
$processImportState->expects($this->exactly(1))->method('finish');
|
||||
$processImportState->expects($this->exactly(2))->method('contactFail')->with(0);
|
||||
$processImportState->expects($this->exactly(2))
|
||||
->method('contactDuplicate')
|
||||
->with($this->callback(static fn (int $duplicates): bool => $duplicates === 0 || $duplicates === 1));
|
||||
$processImportState->expects($this->exactly(2))
|
||||
->method('contactSuccess')
|
||||
->with($this->callback(static fn (int $processed): bool => $processed === 5000 || $processed === 1));
|
||||
$processImportState->method('uuid')->willReturn(Uuid::uuid7());
|
||||
|
||||
$job = new ProcessImport(
|
||||
$this->createTempFile($xml),
|
||||
$processImportState,
|
||||
);
|
||||
$job->handle($createContactCommand);
|
||||
}
|
||||
|
||||
public function testHandleInvalidFile(): void
|
||||
{
|
||||
$xml = <<<'XML'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<data>
|
||||
<item>
|
||||
<first_name>Hari</first_name>
|
||||
<last_name>Seldon</last_name>
|
||||
</item>
|
||||
XML;
|
||||
|
||||
$processImportState = $this->createMock(ProcessImportState::class);
|
||||
$processImportState->expects($this->exactly(1))->method('start');
|
||||
$processImportState->expects($this->exactly(1))->method('finish');
|
||||
$failCallsWithOne = 0;
|
||||
$processImportState->expects($this->atLeastOnce())
|
||||
->method('contactFail')
|
||||
->willReturnCallback(function (int $count) use (&$failCallsWithOne): void {
|
||||
if ($count === 1) {
|
||||
$failCallsWithOne++;
|
||||
}
|
||||
});
|
||||
$processImportState->expects($this->exactly(1))->method('contactDuplicate')->with(0);
|
||||
$processImportState->expects($this->exactly(1))->method('contactSuccess')->with(0);
|
||||
$processImportState->method('uuid')->willReturn(Uuid::uuid7());
|
||||
|
||||
$job = new ProcessImport(
|
||||
$this->createTempFile($xml),
|
||||
$processImportState,
|
||||
);
|
||||
$job->handle($this->createCreateContactCommandMock());
|
||||
$this->assertGreaterThan(0, $failCallsWithOne);
|
||||
}
|
||||
|
||||
public function testHandleInvalidStructure(): void
|
||||
{
|
||||
$xml = <<<'XML'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<data>
|
||||
<item>
|
||||
<email>god@foundation.edu</email>
|
||||
<first_name>Hari</first_name>
|
||||
<last_name>Seldon</last_name>
|
||||
</item>
|
||||
<foo>
|
||||
<email>anubis@badguy.space</email>
|
||||
<first_name>No-imported</first_name>
|
||||
<last_name>Anubis</last_name>
|
||||
</foo>
|
||||
</data>
|
||||
XML;
|
||||
|
||||
$createContactCommand = $this->createCreateContactCommandMock(
|
||||
1,
|
||||
['god@foundation.edu'],
|
||||
new BatchResult(0, 0, 1),
|
||||
);
|
||||
$processImportState = $this->createMock(ProcessImportState::class);
|
||||
$processImportState->expects($this->exactly(1))->method('start');
|
||||
$processImportState->expects($this->exactly(1))->method('finish');
|
||||
$processImportState->expects($this->exactly(1))->method('contactFail')->with(0);
|
||||
$processImportState->expects($this->exactly(1))->method('contactDuplicate')->with(0);
|
||||
$processImportState->expects($this->exactly(1))->method('contactSuccess')->with(1);
|
||||
|
||||
$job = new ProcessImport(
|
||||
$this->createTempFile($xml),
|
||||
$processImportState,
|
||||
);
|
||||
$job->handle($createContactCommand);
|
||||
}
|
||||
|
||||
public function testHandleReallyBadXml(): void
|
||||
{
|
||||
$xml = 'NOT XML';
|
||||
|
||||
$createContactCommand = $this->createCreateContactCommandMock(
|
||||
1,
|
||||
[],
|
||||
new BatchResult(0, 0, 0),
|
||||
);
|
||||
$processImportState = $this->createMock(ProcessImportState::class);
|
||||
$processImportState->expects($this->exactly(1))->method('start');
|
||||
$processImportState->expects($this->exactly(1))->method('finish');
|
||||
$processImportState->expects($this->exactly(1))->method('contactFail')->with(0);
|
||||
$processImportState->expects($this->exactly(1))->method('contactDuplicate')->with(0);
|
||||
$processImportState->expects($this->exactly(1))->method('contactSuccess')->with(0);
|
||||
|
||||
$job = new ProcessImport(
|
||||
$this->createTempFile($xml),
|
||||
$processImportState,
|
||||
);
|
||||
$job->handle($createContactCommand);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Storage::disk('local')->delete($this->tempFiles);
|
||||
|
||||
$this->tempFiles = [];
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
private function createTempFile(string $contents): string
|
||||
{
|
||||
$path = 'process-import/' . Uuid::uuid7()->toString() . '.xml';
|
||||
Storage::disk('local')->put($path, $contents);
|
||||
$this->tempFiles[] = $path;
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
private function createCreateContactCommandMock(
|
||||
?int $expectedCalls = null,
|
||||
array $emails = [],
|
||||
BatchResult|array|null $batchResult = null,
|
||||
): CreateContactCommand {
|
||||
$createContactCommand = $this->createMock(CreateContactCommand::class);
|
||||
|
||||
$expectation = $expectedCalls !== null
|
||||
? $createContactCommand->expects($this->exactly($expectedCalls))->method('multipleExecute')
|
||||
: $createContactCommand->method('multipleExecute');
|
||||
|
||||
if ($expectedCalls !== null) {
|
||||
$expectation
|
||||
->with($this->callback(function (array $intents) use ($emails): bool {
|
||||
if ($emails === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$argEmails = array_map(
|
||||
static fn (CreateContactIntent $intent): string => $intent->email,
|
||||
$intents,
|
||||
);
|
||||
sort($argEmails);
|
||||
sort($emails);
|
||||
|
||||
return $argEmails === $emails;
|
||||
}));
|
||||
}
|
||||
|
||||
if (is_array($batchResult)) {
|
||||
$expectation->willReturnOnConsecutiveCalls(...$batchResult);
|
||||
} elseif ($batchResult instanceof BatchResult) {
|
||||
$expectation->willReturn($batchResult);
|
||||
} else {
|
||||
$expectation->willReturn(new BatchResult(0, 0, 0));
|
||||
}
|
||||
|
||||
return $createContactCommand;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user