- 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>
208 lines
8.6 KiB
PHP
208 lines
8.6 KiB
PHP
<?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();
|
|
});
|