Kundenhoheit: E-Mail-Aenderung mit Bestaetigung, Zuordnungsfehler behoben, Admin-Uebersicht
- E-Mail-Aenderung durch Berater wird erst nach Bestaetigung durch den Kunden aktiv (Link an die neue Adresse, Portal kunde/email-bestaetigen); danach ziehen alle Eintraege des Kunden und seine Abos mit. Admins aendern direkt. - Kundenhoheit: E-Mail eines Beraterkontos oder eines Kunden eines anderen Beraters wird abgelehnt (beim Speichern und bei der Bestaetigung). - Fix CustomerPriority::entryExists: aelteste gepruefte Zuordnung gilt, Eintraege in Pruefung (is_like) dienen nicht mehr als Vorlage. - "Berater zuweisen" voreingestellt auf alle Bestellungen des Kunden. - Neue Admin-Seite Kundenhoheit: offene Pruefungen, Kunden bei mehreren Beratern, abgelehnte/offene E-Mail-Aenderungen; Zaehler im Menue. - Neue Tabelle shopping_user_email_changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ae59679150
commit
9ad69a3218
32 changed files with 2246 additions and 119 deletions
372
tests/Feature/CustomerEmailChangeTest.php
Normal file
372
tests/Feature/CustomerEmailChangeTest.php
Normal file
|
|
@ -0,0 +1,372 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\CustomerController as AdminCustomerController;
|
||||
use App\Http\Controllers\User\CustomerController as ConsultantCustomerController;
|
||||
use App\Http\Controllers\Web\CustomerEmailChangeController;
|
||||
use App\Mail\CustomerEmailChangeBlockedMail;
|
||||
use App\Mail\CustomerEmailChangeMail;
|
||||
use App\Models\Country;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\ShoppingUserEmailChange;
|
||||
use App\Models\UserAbo;
|
||||
use App\Services\CustomerEmailChangeService;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Mail::fake();
|
||||
URL::resolveMissingNamedRoutesUsing(
|
||||
fn (string $name, array $parameters) => '/'.$name.'/'.implode('/', $parameters)
|
||||
);
|
||||
|
||||
$this->country = Country::create([
|
||||
'code' => 'DE', 'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland',
|
||||
'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия',
|
||||
]);
|
||||
|
||||
$this->member = User::forceCreate([
|
||||
'email' => 'cec-member-'.uniqid('', true).'@example.com',
|
||||
'password' => Hash::make('secret'),
|
||||
'lang' => 'de',
|
||||
]);
|
||||
|
||||
$this->service = app(CustomerEmailChangeService::class);
|
||||
});
|
||||
|
||||
function cecCustomer(array $attributes = []): ShoppingUser
|
||||
{
|
||||
return ShoppingUser::create(array_merge([
|
||||
'auth_user_id' => null,
|
||||
'member_id' => test()->member->id,
|
||||
'number' => 7100,
|
||||
'language' => 'de',
|
||||
'billing_salutation' => 'mr',
|
||||
'billing_firstname' => 'Sascha',
|
||||
'billing_lastname' => 'Frey',
|
||||
'billing_address' => 'Weg 1',
|
||||
'billing_zipcode' => '11111',
|
||||
'billing_city' => 'Stadt',
|
||||
'billing_email' => 'alt@example.com',
|
||||
'billing_country_id' => test()->country->id,
|
||||
'shipping_country_id' => test()->country->id,
|
||||
'same_as_billing' => true,
|
||||
'faker_mail' => false,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
function cecAbo(ShoppingUser $customer, array $attributes = []): UserAbo
|
||||
{
|
||||
return UserAbo::create(array_merge([
|
||||
'member_id' => test()->member->id,
|
||||
'shopping_user_id' => $customer->id,
|
||||
'email' => $customer->billing_email,
|
||||
'is_for' => 'ot',
|
||||
'payone_userid' => 0,
|
||||
'clearingtype' => 'stripe',
|
||||
'abo_interval' => 5,
|
||||
'status' => 3,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a controller action that reads its input from the Request facade.
|
||||
*/
|
||||
function cecCallStore(object $controller, int $id, array $input): mixed
|
||||
{
|
||||
$request = Request::create('/customer/edit/'.$id, 'POST', $input);
|
||||
$request->setLaravelSession(app('session.store'));
|
||||
app()->instance('request', $request);
|
||||
Facade::clearResolvedInstance('request');
|
||||
|
||||
return $controller->store($id);
|
||||
}
|
||||
|
||||
function cecFormData(ShoppingUser $customer, string $email): array
|
||||
{
|
||||
return [
|
||||
'action' => 'shopping-user-store',
|
||||
'billing_salutation' => $customer->billing_salutation,
|
||||
'billing_firstname' => $customer->billing_firstname,
|
||||
'billing_lastname' => $customer->billing_lastname,
|
||||
'billing_address' => $customer->billing_address,
|
||||
'billing_zipcode' => $customer->billing_zipcode,
|
||||
'billing_city' => 'Neustadt',
|
||||
'billing_country_id' => $customer->billing_country_id,
|
||||
'billing_email' => $email,
|
||||
'same_as_billing' => '1',
|
||||
];
|
||||
}
|
||||
|
||||
// --- Wann ist eine Bestätigung nötig? ---
|
||||
|
||||
it('verlangt eine Bestätigung nur beim Ersetzen einer echten Adresse', function () {
|
||||
$customer = cecCustomer();
|
||||
|
||||
expect($this->service->requiresConfirmation($customer, 'neu@example.com'))->toBeTrue()
|
||||
->and($this->service->requiresConfirmation($customer, ' ALT@example.com '))->toBeFalse()
|
||||
->and($this->service->requiresConfirmation($customer, ''))->toBeFalse()
|
||||
->and($this->service->requiresConfirmation($customer, null))->toBeFalse();
|
||||
|
||||
$placeholder = cecCustomer(['number' => 7101, 'faker_mail' => true, 'billing_email' => '123-faker@mivita.care']);
|
||||
expect($this->service->requiresConfirmation($placeholder, 'neu@example.com'))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Anfrage ---
|
||||
|
||||
it('schickt den Bestätigungslink an die neue Adresse und lässt die alte aktiv', function () {
|
||||
$customer = cecCustomer();
|
||||
$abo = cecAbo($customer);
|
||||
|
||||
$change = $this->service->request($customer, ' neu@example.com ', $this->member->id);
|
||||
|
||||
expect($change->new_email)->toBe('neu@example.com')
|
||||
->and($change->old_email)->toBe('alt@example.com')
|
||||
->and($change->requested_by_user_id)->toBe($this->member->id)
|
||||
->and($change->isConfirmable())->toBeTrue()
|
||||
->and($customer->fresh()->billing_email)->toBe('alt@example.com')
|
||||
->and($abo->fresh()->email)->toBe('alt@example.com')
|
||||
->and($this->service->pendingFor($customer)?->id)->toBe($change->id);
|
||||
|
||||
Mail::assertSent(CustomerEmailChangeMail::class, function (CustomerEmailChangeMail $mail) use ($change) {
|
||||
return $mail->hasTo('neu@example.com')
|
||||
&& ShoppingUserEmailChange::hashToken($mail->token) === $change->token_hash;
|
||||
});
|
||||
});
|
||||
|
||||
it('enthält in der Mail den Bestätigungslink ins Kundenportal', function () {
|
||||
$html = (new CustomerEmailChangeMail(cecCustomer(), 'abc123'))->render();
|
||||
|
||||
expect($html)->toContain('/kunde/email-bestaetigen/abc123')
|
||||
->and($html)->toContain(__('customer.email_change_mail_button'))
|
||||
->and($html)->toContain('Sascha');
|
||||
});
|
||||
|
||||
it('ersetzt eine ältere offene Anfrage desselben Kunden', function () {
|
||||
$customer = cecCustomer();
|
||||
$first = $this->service->request($customer, 'erst@example.com');
|
||||
$second = $this->service->request($customer, 'zweit@example.com');
|
||||
|
||||
expect(ShoppingUserEmailChange::find($first->id))->toBeNull()
|
||||
->and($this->service->pendingFor($customer)->id)->toBe($second->id);
|
||||
});
|
||||
|
||||
// --- Bestätigung ---
|
||||
|
||||
it('übernimmt nach Bestätigung die Adresse für alle Einträge des Kunden und seine Abos', function () {
|
||||
$older = cecCustomer();
|
||||
$latest = cecCustomer();
|
||||
$abo = cecAbo($older);
|
||||
$endedAbo = cecAbo($older, ['status' => 5]);
|
||||
$olderAddress = cecCustomer(['billing_email' => 'ganz-alt@example.com']);
|
||||
$otherCustomer = cecCustomer(['number' => 7200]);
|
||||
$otherAbo = cecAbo($otherCustomer);
|
||||
|
||||
$change = $this->service->request($latest, 'neu@example.com');
|
||||
|
||||
expect($this->service->confirm($change))->toBe(CustomerEmailChangeService::STATUS_CONFIRMED);
|
||||
|
||||
expect($older->fresh()->billing_email)->toBe('neu@example.com')
|
||||
->and($latest->fresh()->billing_email)->toBe('neu@example.com')
|
||||
->and($abo->fresh()->email)->toBe('neu@example.com')
|
||||
->and($endedAbo->fresh()->email)->toBe('neu@example.com')
|
||||
->and($olderAddress->fresh()->billing_email)->toBe('ganz-alt@example.com')
|
||||
->and($otherCustomer->fresh()->billing_email)->toBe('alt@example.com')
|
||||
->and($otherAbo->fresh()->email)->toBe('alt@example.com');
|
||||
|
||||
$change->refresh();
|
||||
expect($change->confirmed_at)->not->toBeNull()
|
||||
->and($change->token_hash)->toBeNull()
|
||||
->and($this->service->confirm($change))->toBe(CustomerEmailChangeService::STATUS_INVALID);
|
||||
});
|
||||
|
||||
it('lehnt abgelaufene Links ab', function () {
|
||||
$customer = cecCustomer();
|
||||
$change = $this->service->request($customer, 'neu@example.com');
|
||||
$change->forceFill(['expires_at' => now()->subMinute()])->save();
|
||||
|
||||
expect($this->service->confirm($change->fresh()))->toBe(CustomerEmailChangeService::STATUS_INVALID)
|
||||
->and($customer->fresh()->billing_email)->toBe('alt@example.com');
|
||||
});
|
||||
|
||||
it('lehnt einen Link ab, wenn die Adresse inzwischen anders geändert wurde', function () {
|
||||
$customer = cecCustomer();
|
||||
$change = $this->service->request($customer, 'neu@example.com');
|
||||
$this->service->changeDirectly($customer, 'admin@example.com');
|
||||
|
||||
expect($this->service->confirm($change->fresh() ?? $change))->toBe(CustomerEmailChangeService::STATUS_INVALID)
|
||||
->and($customer->fresh()->billing_email)->toBe('admin@example.com');
|
||||
});
|
||||
|
||||
// --- Portal-Seite ---
|
||||
|
||||
it('bestätigt erst per POST und zeigt bei ungültigem Link eine Fehlerseite', function () {
|
||||
$customer = cecCustomer();
|
||||
$this->service->request($customer, 'neu@example.com');
|
||||
$token = null;
|
||||
Mail::assertSent(CustomerEmailChangeMail::class, function (CustomerEmailChangeMail $mail) use (&$token) {
|
||||
$token = $mail->token;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
$controller = new CustomerEmailChangeController;
|
||||
|
||||
$invalid = $controller->show('gibt-es-nicht', $this->service);
|
||||
expect($invalid->name())->toBe('portal.customer.email_change_done')
|
||||
->and($invalid->getData()['status'])->toBe(CustomerEmailChangeService::STATUS_INVALID);
|
||||
|
||||
$page = $controller->show($token, $this->service);
|
||||
expect($page->name())->toBe('portal.customer.email_change_confirm')
|
||||
->and($customer->fresh()->billing_email)->toBe('alt@example.com');
|
||||
|
||||
$done = $controller->store($token, $this->service);
|
||||
expect($done->getData()['status'])->toBe(CustomerEmailChangeService::STATUS_CONFIRMED)
|
||||
->and($customer->fresh()->billing_email)->toBe('neu@example.com');
|
||||
|
||||
expect($controller->store($token, $this->service)->getData()['status'])->toBe(CustomerEmailChangeService::STATUS_INVALID);
|
||||
});
|
||||
|
||||
// --- CRM ---
|
||||
|
||||
it('übernimmt eine vom Berater geänderte E-Mail erst nach Bestätigung, andere Felder sofort', function () {
|
||||
$customer = cecCustomer();
|
||||
$abo = cecAbo($customer);
|
||||
$this->actingAs($this->member);
|
||||
|
||||
cecCallStore(app(ConsultantCustomerController::class), $customer->id, cecFormData($customer, 'neu@example.com'));
|
||||
|
||||
$customer->refresh();
|
||||
expect($customer->billing_email)->toBe('alt@example.com')
|
||||
->and($customer->billing_city)->toBe('Neustadt')
|
||||
->and($abo->fresh()->email)->toBe('alt@example.com')
|
||||
->and($this->service->pendingFor($customer)?->new_email)->toBe('neu@example.com')
|
||||
->and(session('custom-error'))->toContain('neu@example.com');
|
||||
|
||||
Mail::assertSent(CustomerEmailChangeMail::class, fn ($mail) => $mail->hasTo('neu@example.com'));
|
||||
});
|
||||
|
||||
it('startet keine Bestätigung, wenn der Berater die E-Mail nicht ändert', function () {
|
||||
$customer = cecCustomer();
|
||||
$this->actingAs($this->member);
|
||||
|
||||
cecCallStore(app(ConsultantCustomerController::class), $customer->id, cecFormData($customer, 'alt@example.com'));
|
||||
|
||||
expect($this->service->pendingFor($customer))->toBeNull()
|
||||
->and($customer->fresh()->billing_city)->toBe('Neustadt');
|
||||
Mail::assertNotSent(CustomerEmailChangeMail::class);
|
||||
});
|
||||
|
||||
it('lässt den Admin die E-Mail direkt ändern und zieht die Abos mit', function () {
|
||||
$customer = cecCustomer();
|
||||
$abo = cecAbo($customer);
|
||||
$admin = User::forceCreate([
|
||||
'email' => 'cec-admin-'.uniqid('', true).'@example.com',
|
||||
'password' => Hash::make('secret'),
|
||||
'lang' => 'de',
|
||||
'admin' => 1,
|
||||
]);
|
||||
$this->actingAs($admin);
|
||||
|
||||
cecCallStore(app(AdminCustomerController::class), $customer->id, cecFormData($customer, 'neu@example.com'));
|
||||
|
||||
expect($customer->fresh()->billing_email)->toBe('neu@example.com')
|
||||
->and($abo->fresh()->email)->toBe('neu@example.com')
|
||||
->and(ShoppingUserEmailChange::where('shopping_user_id', $customer->id)->whereNotNull('confirmed_at')->value('requested_by_user_id'))->toBe($admin->id);
|
||||
Mail::assertNotSent(CustomerEmailChangeMail::class);
|
||||
});
|
||||
|
||||
// --- Kundenhoheit ---
|
||||
|
||||
function cecOtherConsultant(array $attributes = []): User
|
||||
{
|
||||
return User::forceCreate(array_merge([
|
||||
'email' => 'cec-other-'.uniqid('', true).'@example.com',
|
||||
'password' => Hash::make('secret'),
|
||||
'lang' => 'de',
|
||||
'active' => 0,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
it('erkennt, wenn die E-Mail einem Kunden eines anderen Beraters oder einem Beraterkonto gehört', function () {
|
||||
$customer = cecCustomer();
|
||||
$other = cecOtherConsultant();
|
||||
cecCustomer(['number' => 7300, 'member_id' => $other->id, 'billing_email' => 'fremd@example.com']);
|
||||
cecCustomer(['number' => 7301, 'billing_email' => 'eigener@example.com']);
|
||||
cecCustomer(['number' => 7302, 'member_id' => $other->id, 'billing_email' => 'geloescht@example.com'])
|
||||
->forceFill(['gdpr_deleted_at' => now()])->save();
|
||||
cecOtherConsultant(['email' => 'berater@example.com', 'active' => 1]);
|
||||
cecOtherConsultant(['email' => 'inaktiv@example.com', 'active' => 0]);
|
||||
|
||||
expect($this->service->ownershipConflict($customer, 'fremd@example.com'))->toBe(CustomerEmailChangeService::CONFLICT_OTHER_CONSULTANT)
|
||||
->and($this->service->ownershipConflict($customer, ' berater@example.com '))->toBe(CustomerEmailChangeService::CONFLICT_CONSULTANT_ACCOUNT)
|
||||
->and($this->service->ownershipConflict($customer, 'eigener@example.com'))->toBeNull()
|
||||
->and($this->service->ownershipConflict($customer, 'geloescht@example.com'))->toBeNull()
|
||||
->and($this->service->ownershipConflict($customer, 'inaktiv@example.com'))->toBeNull()
|
||||
->and($this->service->ownershipConflict($customer, 'unbekannt@example.com'))->toBeNull();
|
||||
});
|
||||
|
||||
it('lehnt die Änderung durch den Berater ab, wenn die E-Mail einem anderen Berater zugeordnet ist', function () {
|
||||
$customer = cecCustomer();
|
||||
$abo = cecAbo($customer);
|
||||
$other = cecOtherConsultant();
|
||||
cecCustomer(['number' => 7300, 'member_id' => $other->id, 'billing_email' => 'fremd@example.com']);
|
||||
$this->actingAs($this->member);
|
||||
|
||||
cecCallStore(app(ConsultantCustomerController::class), $customer->id, cecFormData($customer, 'fremd@example.com'));
|
||||
|
||||
$customer->refresh();
|
||||
expect($customer->billing_email)->toBe('alt@example.com')
|
||||
->and($customer->billing_city)->toBe('Neustadt')
|
||||
->and((bool) $customer->is_like)->toBeFalse()
|
||||
->and($abo->fresh()->email)->toBe('alt@example.com')
|
||||
->and(ShoppingUserEmailChange::count())->toBe(1)
|
||||
->and(ShoppingUserEmailChange::blockedOpen()->first()?->new_email)->toBe('fremd@example.com')
|
||||
->and(session('custom-error'))->toBe(__('customer.email_change_blocked', ['email' => 'fremd@example.com']));
|
||||
|
||||
Mail::assertNotSent(CustomerEmailChangeMail::class);
|
||||
Mail::assertSent(CustomerEmailChangeBlockedMail::class, function (CustomerEmailChangeBlockedMail $mail) use ($other) {
|
||||
return $mail->hasTo(config('app.priority_mail'))
|
||||
&& $mail->newEmail === 'fremd@example.com'
|
||||
&& $mail->owners === [$other->email]
|
||||
&& $mail->requestedBy === $this->member->email;
|
||||
});
|
||||
});
|
||||
|
||||
it('lehnt die Bestätigung ab, wenn die E-Mail inzwischen einem anderen Berater zugeordnet wurde', function () {
|
||||
$customer = cecCustomer();
|
||||
$abo = cecAbo($customer);
|
||||
$this->service->request($customer, 'neu@example.com', $this->member->id);
|
||||
$token = null;
|
||||
Mail::assertSent(CustomerEmailChangeMail::class, function (CustomerEmailChangeMail $mail) use (&$token) {
|
||||
$token = $mail->token;
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
cecCustomer(['number' => 7300, 'member_id' => cecOtherConsultant()->id, 'billing_email' => 'neu@example.com']);
|
||||
$controller = new CustomerEmailChangeController;
|
||||
|
||||
expect($controller->show($token, $this->service)->getData()['status'])->toBe(CustomerEmailChangeService::STATUS_CONFLICT)
|
||||
->and($controller->store($token, $this->service)->getData()['status'])->toBe(CustomerEmailChangeService::STATUS_CONFLICT)
|
||||
->and($customer->fresh()->billing_email)->toBe('alt@example.com')
|
||||
->and($abo->fresh()->email)->toBe('alt@example.com')
|
||||
->and($this->service->findConfirmable($token))->toBeNull();
|
||||
|
||||
Mail::assertSent(CustomerEmailChangeBlockedMail::class);
|
||||
});
|
||||
|
||||
it('rendert die Info-Mail an die Kundenhoheit', function () {
|
||||
$html = (new CustomerEmailChangeBlockedMail(cecCustomer(), 'fremd@example.com', 'berater@example.com', ['b2@example.com']))->render();
|
||||
|
||||
expect($html)->toContain('fremd@example.com')
|
||||
->and($html)->toContain('b2@example.com')
|
||||
->and($html)->toContain('alt@example.com');
|
||||
});
|
||||
85
tests/Feature/CustomerPriorityEntryExistsTest.php
Normal file
85
tests/Feature/CustomerPriorityEntryExistsTest.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Services\CustomerPriority;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Mail::fake();
|
||||
|
||||
$this->country = Country::create([
|
||||
'code' => 'DE', 'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland',
|
||||
'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия',
|
||||
]);
|
||||
|
||||
$this->owner = cpeConsultant();
|
||||
$this->other = cpeConsultant();
|
||||
});
|
||||
|
||||
function cpeConsultant(): User
|
||||
{
|
||||
return User::forceCreate([
|
||||
'email' => 'cpe-'.uniqid('', true).'@example.com',
|
||||
'password' => Hash::make('secret'),
|
||||
'lang' => 'de',
|
||||
'active' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
function cpeEntry(array $attributes = []): ShoppingUser
|
||||
{
|
||||
return ShoppingUser::create(array_merge([
|
||||
'auth_user_id' => null,
|
||||
'billing_firstname' => 'Karola',
|
||||
'billing_lastname' => 'Home',
|
||||
'billing_address' => 'Weg 1',
|
||||
'billing_zipcode' => '11111',
|
||||
'billing_city' => 'Stadt',
|
||||
'billing_email' => 'karola@example.com',
|
||||
'billing_country_id' => test()->country->id,
|
||||
'shipping_country_id' => test()->country->id,
|
||||
'is_from' => 'shopping',
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
it('ordnet eine neue Bestellung der ältesten geprüften Zuordnung zu, nicht einem Eintrag in Prüfung', function () {
|
||||
$established = cpeEntry(['member_id' => $this->owner->id, 'number' => 100]);
|
||||
cpeEntry(['member_id' => $this->other->id, 'number' => 200, 'is_like' => true]);
|
||||
$new = cpeEntry();
|
||||
|
||||
expect(CustomerPriority::checkOne($new))->toBe('exists');
|
||||
|
||||
$new->refresh();
|
||||
expect($new->member_id)->toBe($this->owner->id)
|
||||
->and($new->number)->toBe($established->number);
|
||||
});
|
||||
|
||||
it('nimmt bei mehreren geprüften Zuordnungen die älteste', function () {
|
||||
cpeEntry(['member_id' => $this->owner->id, 'number' => 100]);
|
||||
cpeEntry(['member_id' => $this->other->id, 'number' => 200]);
|
||||
$new = cpeEntry();
|
||||
|
||||
CustomerPriority::checkOne($new);
|
||||
|
||||
expect($new->fresh()->member_id)->toBe($this->owner->id)
|
||||
->and($new->fresh()->number)->toBe(100);
|
||||
});
|
||||
|
||||
it('übernimmt keine Zuordnung von einem Eintrag in Prüfung, sondern prüft den Kunden selbst', function () {
|
||||
cpeEntry(['member_id' => $this->other->id, 'number' => 200, 'is_like' => true]);
|
||||
$new = cpeEntry();
|
||||
|
||||
expect(CustomerPriority::checkOne($new, false, false))->toStartWith('like');
|
||||
|
||||
$new->refresh();
|
||||
expect((bool) $new->is_like)->toBeTrue()
|
||||
->and($new->number)->toBeNull()
|
||||
->and($new->member_id)->toBeNull();
|
||||
});
|
||||
208
tests/Feature/CustomerPriorityOverviewTest.php
Normal file
208
tests/Feature/CustomerPriorityOverviewTest.php
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\Admin\CustomerPriorityController;
|
||||
use App\Http\Controllers\CustomerController as AdminCustomerController;
|
||||
use App\Models\Country;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Services\CustomerEmailChangeService;
|
||||
use App\Services\CustomerPriorityOverviewService;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Mail::fake();
|
||||
URL::resolveMissingNamedRoutesUsing(
|
||||
fn (string $name, array $parameters) => '/'.$name.'/'.implode('/', $parameters)
|
||||
);
|
||||
|
||||
$this->country = Country::create([
|
||||
'code' => 'DE', 'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland',
|
||||
'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия',
|
||||
]);
|
||||
$this->first = cpoConsultant();
|
||||
$this->second = cpoConsultant();
|
||||
$this->overview = app(CustomerPriorityOverviewService::class);
|
||||
});
|
||||
|
||||
function cpoConsultant(array $attributes = []): User
|
||||
{
|
||||
return User::forceCreate(array_merge([
|
||||
'email' => 'cpo-'.uniqid('', true).'@example.com',
|
||||
'password' => Hash::make('secret'),
|
||||
'lang' => 'de',
|
||||
'active' => 1,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
function cpoEntry(array $attributes = []): ShoppingUser
|
||||
{
|
||||
return ShoppingUser::create(array_merge([
|
||||
'auth_user_id' => null,
|
||||
'member_id' => test()->first->id,
|
||||
'number' => 100,
|
||||
'billing_firstname' => 'Karola',
|
||||
'billing_lastname' => 'Home',
|
||||
'billing_address' => 'Weg 1',
|
||||
'billing_zipcode' => '11111',
|
||||
'billing_city' => 'Stadt',
|
||||
'billing_email' => 'karola@example.com',
|
||||
'billing_country_id' => test()->country->id,
|
||||
'shipping_country_id' => test()->country->id,
|
||||
'faker_mail' => false,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Datatable endpoints read their parameters from the current request.
|
||||
*/
|
||||
function cpoDatatable(string $method): array
|
||||
{
|
||||
$request = Request::create('/datatable', 'GET', ['draw' => 1, 'start' => 0, 'length' => 50, 'search' => ['value' => '']]);
|
||||
app()->instance('request', $request);
|
||||
Facade::clearResolvedInstance('request');
|
||||
|
||||
return app(CustomerPriorityController::class)->{$method}()->getData(true);
|
||||
}
|
||||
|
||||
// --- Kunde bei mehreren Beratern ---
|
||||
|
||||
it('listet Kunden, deren E-Mail bei mehreren Beratern liegt, und markiert die älteste Zuordnung', function () {
|
||||
$oldest = cpoEntry();
|
||||
cpoEntry();
|
||||
$other = cpoEntry(['member_id' => $this->second->id, 'number' => 200]);
|
||||
cpoEntry(['billing_email' => 'eindeutig@example.com', 'number' => 300]);
|
||||
|
||||
$conflicts = $this->overview->conflicts();
|
||||
|
||||
expect($conflicts)->toHaveCount(1);
|
||||
$row = $conflicts->first();
|
||||
expect($row['email'])->toBe('karola@example.com')
|
||||
->and($row['groups'])->toHaveCount(2)
|
||||
->and($row['groups'][0]['member_id'])->toBe($this->first->id)
|
||||
->and($row['groups'][0]['oldest'])->toBeTrue()
|
||||
->and($row['groups'][0]['entries'])->toBe(2)
|
||||
->and($row['groups'][0]['latest_id'])->not->toBe($oldest->id)
|
||||
->and($row['groups'][1]['latest_id'])->toBe($other->id)
|
||||
->and($row['groups'][1]['oldest'])->toBeFalse();
|
||||
});
|
||||
|
||||
it('lässt offene Prüfungen, Beraterkonten und gelöschte Einträge aus der Konfliktliste', function () {
|
||||
cpoEntry();
|
||||
cpoEntry(['member_id' => $this->second->id, 'number' => 200, 'is_like' => true]);
|
||||
|
||||
cpoEntry(['billing_email' => 'berater@example.com']);
|
||||
cpoEntry(['billing_email' => 'berater@example.com', 'member_id' => $this->second->id, 'number' => 201]);
|
||||
cpoConsultant(['email' => 'berater@example.com']);
|
||||
|
||||
cpoEntry(['billing_email' => 'weg@example.com']);
|
||||
cpoEntry(['billing_email' => 'weg@example.com', 'member_id' => $this->second->id, 'number' => 202])
|
||||
->forceFill(['gdpr_deleted_at' => now()])->save();
|
||||
|
||||
expect($this->overview->conflicts())->toBeEmpty();
|
||||
});
|
||||
|
||||
// --- Offene Prüfungen ---
|
||||
|
||||
it('zählt offene Prüfungen und abgelehnte E-Mail-Änderungen für das Menü', function () {
|
||||
$review = cpoEntry(['is_like' => true]);
|
||||
cpoEntry(['is_like' => true])->forceFill(['gdpr_deleted_at' => now()])->save();
|
||||
cpoEntry(['number' => 101]);
|
||||
app(CustomerEmailChangeService::class)->reportConflict($review, 'fremd@example.com');
|
||||
|
||||
expect($this->overview->reviewsQuery()->pluck('id')->all())->toBe([$review->id])
|
||||
->and($this->overview->openCount())->toBe(2)
|
||||
->and($this->overview->counts())->toBe(['reviews' => 1, 'conflicts' => 0, 'email_changes' => 1]);
|
||||
});
|
||||
|
||||
it('schließt einen Prüffall ohne Berater oder Nummer zu ändern', function () {
|
||||
$review = cpoEntry(['is_like' => true, 'member_id' => $this->second->id, 'number' => 555]);
|
||||
$review->setNotice('like', [99 => 100]);
|
||||
|
||||
app(CustomerPriorityController::class)->keepAssignment($review->id);
|
||||
|
||||
$review->refresh();
|
||||
expect($review->is_like)->toBeFalse()
|
||||
->and($review->getNotice('like'))->toBeFalse()
|
||||
->and($review->member_id)->toBe($this->second->id)
|
||||
->and($review->number)->toBe(555);
|
||||
});
|
||||
|
||||
// --- E-Mail-Änderungen ---
|
||||
|
||||
it('zeigt abgelehnte und wartende E-Mail-Änderungen, bis sie erledigt oder verworfen sind', function () {
|
||||
$service = app(CustomerEmailChangeService::class);
|
||||
$customer = cpoEntry();
|
||||
$blocked = $service->reportConflict($customer, 'fremd@example.com', $this->first->id);
|
||||
$pending = $service->request(cpoEntry(['number' => 101, 'billing_email' => 'b@example.com']), 'neu@example.com');
|
||||
$service->changeDirectly(cpoEntry(['number' => 102, 'billing_email' => 'c@example.com']), 'admin@example.com');
|
||||
|
||||
expect($blocked->blocked_at)->not->toBeNull()
|
||||
->and($this->overview->emailChangesQuery()->pluck('id')->sort()->values()->all())->toBe([$blocked->id, $pending->id]);
|
||||
|
||||
$this->actingAs($this->first);
|
||||
$controller = app(CustomerPriorityController::class);
|
||||
$controller->resolveEmailChange($blocked->id, $service);
|
||||
$controller->discardEmailChange($pending->id, $service);
|
||||
|
||||
expect($blocked->fresh()->resolved_by_user_id)->toBe($this->first->id)
|
||||
->and($service->findConfirmable('egal'))->toBeNull()
|
||||
->and($pending->fresh()->isConfirmable())->toBeFalse()
|
||||
->and($this->overview->emailChangesQuery()->count())->toBe(0);
|
||||
});
|
||||
|
||||
// --- Datatables ---
|
||||
|
||||
it('liefert die drei Listen als Datatable', function () {
|
||||
$review = cpoEntry(['is_like' => true, 'billing_email' => 'pruefung@example.com', 'number' => 400]);
|
||||
cpoEntry();
|
||||
cpoEntry(['member_id' => $this->second->id, 'number' => 200]);
|
||||
app(CustomerEmailChangeService::class)->reportConflict($review, 'fremd@example.com');
|
||||
|
||||
$reviews = cpoDatatable('reviewsDatatable');
|
||||
$conflicts = cpoDatatable('conflictsDatatable');
|
||||
$changes = cpoDatatable('emailChangesDatatable');
|
||||
|
||||
expect($reviews['recordsTotal'])->toBe(1)
|
||||
->and($reviews['data'][0]['customer'])->toContain('pruefung@example.com')
|
||||
->and($reviews['data'][0]['action'])->toContain('shopping-user-is-like-member')
|
||||
->and($conflicts['recordsTotal'])->toBe(1)
|
||||
->and($conflicts['data'][0]['groups'])->toContain('älteste Zuordnung')
|
||||
->and($conflicts['data'][0]['groups'])->toContain('shopping-user-change-member')
|
||||
->and($changes['recordsTotal'])->toBe(1)
|
||||
->and($changes['data'][0]['status'])->toContain('abgelehnt');
|
||||
});
|
||||
|
||||
// --- Berater zuweisen aus der Übersicht ---
|
||||
|
||||
it('führt nach "Berater zuweisen" zurück in die Übersicht und hängt alle Einträge des Kunden um', function () {
|
||||
cpoEntry();
|
||||
$moved = cpoEntry(['member_id' => $this->second->id, 'number' => 200]);
|
||||
cpoEntry(['member_id' => $this->second->id, 'number' => 200]);
|
||||
$back = route('admin_customer_priority', ['conflicts']);
|
||||
|
||||
$request = Request::create('/admin/customer/edit/'.$moved->id, 'POST', [
|
||||
'action' => 'shopping-user-change-member',
|
||||
'id' => $moved->id,
|
||||
'change_member_key' => config('mivita.edit_data_pass'),
|
||||
'change_member_id' => $this->first->id,
|
||||
'customer_set_member_for' => '1',
|
||||
'back' => $back,
|
||||
]);
|
||||
$request->setLaravelSession(app('session.store'));
|
||||
app()->instance('request', $request);
|
||||
Facade::clearResolvedInstance('request');
|
||||
|
||||
$response = app(AdminCustomerController::class)->store($moved->id);
|
||||
|
||||
expect($response->getTargetUrl())->toEndWith('/admin_customer_priority/conflicts')
|
||||
->and(ShoppingUser::where('number', '!=', 100)->where('member_id', $this->second->id)->count())->toBe(0)
|
||||
->and($this->overview->conflicts())->toBeEmpty();
|
||||
});
|
||||
77
tests/Feature/CustomerPriorityPageTest.php
Normal file
77
tests/Feature/CustomerPriorityPageTest.php
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->previousHttpHost = $_SERVER['HTTP_HOST'] ?? null;
|
||||
Mail::fake();
|
||||
$_SERVER['HTTP_HOST'] = config('domains.domains.crm.host');
|
||||
$this->refreshApplication();
|
||||
$this->artisan('migrate');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
// Host wieder zurücksetzen, sonst booten nachfolgende Tests mit den CRM-Routen.
|
||||
if ($this->previousHttpHost === null) {
|
||||
unset($_SERVER['HTTP_HOST']);
|
||||
} else {
|
||||
$_SERVER['HTTP_HOST'] = $this->previousHttpHost;
|
||||
}
|
||||
});
|
||||
|
||||
function cppUser(array $attributes = []): User
|
||||
{
|
||||
return User::forceCreate(array_merge([
|
||||
'email' => 'cpp-'.uniqid('', true).'@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
'active' => 1,
|
||||
'wizard' => 100,
|
||||
'payment_account' => now()->addYear(),
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
it('zeigt Admins die Übersicht Kundenhoheit mit allen Reitern und dem Zähler im Menü', function (string $tab, string $text) {
|
||||
$admin = cppUser(['admin' => 2]);
|
||||
$country = Country::first() ?? Country::create([
|
||||
'code' => 'DE', 'phone' => '49', 'en' => 'Germany', 'de' => 'Deutschland',
|
||||
'es' => 'Alemania', 'fr' => 'Allemagne', 'it' => 'Germania', 'ru' => 'Германия',
|
||||
]);
|
||||
ShoppingUser::create([
|
||||
'member_id' => $admin->id,
|
||||
'number' => 71001,
|
||||
'is_like' => true,
|
||||
'billing_firstname' => 'Paula',
|
||||
'billing_lastname' => 'Prüf',
|
||||
'billing_email' => 'paula@example.com',
|
||||
'billing_country_id' => $country->id,
|
||||
'shipping_country_id' => $country->id,
|
||||
]);
|
||||
|
||||
$this->actingAs($admin)->get(route('admin_customer_priority', [$tab]))
|
||||
->assertOk()
|
||||
->assertSee('Kundenhoheit')
|
||||
->assertSee($text)
|
||||
->assertSee('id="datatables-customer-priority"', false)
|
||||
->assertSee('<div class="badge badge-danger">1</div>', false);
|
||||
})->with([
|
||||
'Prüfungen' => ['reviews', 'Prüfen & zuordnen'],
|
||||
'Konflikte' => ['conflicts', 'älteste Zuordnung'],
|
||||
'E-Mail' => ['email-changes', 'wartet auf Bestätigung'],
|
||||
]);
|
||||
|
||||
it('kennt nur die drei Reiter', function () {
|
||||
$this->actingAs(cppUser(['admin' => 2]))->get(route('admin_customer_priority', ['gibt-es-nicht']))
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
it('sperrt die Übersicht für Berater ohne Admin-Rechte', function () {
|
||||
$this->actingAs(cppUser())->get(route('admin_customer_priority'))
|
||||
->assertRedirect();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue