'/'.$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'); });