mivita/tests/Feature/Newsletter/NewsletterListSavedTest.php
Kevin d21d94a060 Newsletter-Listen: Empfaengerlisten filtern, speichern und exportieren
Admin-Bereich "Newsletter-Listen" (Plan dev/_tickets/entwicklungsplan-
newsletter-listen.md): Berater und Kunden nach Filtern zusammenstellen,
als Liste speichern und wieder laden, Export als XLSX/CSV. Jeder Export
wird protokolliert (newsletter_list_exports).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-23 15:44:09 +00:00

256 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Http\Controllers\Admin\NewsletterListController;
use App\Http\Requests\Newsletter\NewsletterListFilterRequest;
use App\Http\Requests\Newsletter\StoreNewsletterListRequest;
use App\Models\NewsletterList;
use App\Models\NewsletterListExport;
use App\Models\ShoppingUser;
use App\Services\Newsletter\NewsletterListFilter;
use App\Services\Newsletter\NewsletterListService;
use App\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Facades\Route;
use Illuminate\Validation\ValidationException;
use Maatwebsite\Excel\Facades\Excel;
use Tests\TestCase;
uses(TestCase::class, RefreshDatabase::class);
/**
* Domain-Routen werden im Test-Kontext nicht geladen (Host ist nicht die CRM-Domain).
* Für die Weiterleitungen und Links des Controllers registrieren wir die CRM-Routen hier.
*/
beforeEach(function () {
if (! app('router')->has('admin_newsletter_lists_load')) {
Route::group([], base_path('routes/domains/crm.php'));
app('router')->getRoutes()->refreshNameLookups();
}
});
function newsletterSavedAdmin(): User
{
return User::forceCreate([
'email' => 'admin-saved-'.uniqid('', true).'@example.com',
'password' => bcrypt('secret'),
'admin' => 2,
'lang' => 'de',
'active' => 1,
'blocked' => 0,
]);
}
/**
* @param array<string, mixed> $params
*/
function newsletterStoreRequest(User $user, array $params = []): StoreNewsletterListRequest
{
$request = StoreNewsletterListRequest::create('/admin/newsletter-lists/save', 'POST', $params);
$request->setUserResolver(fn () => $user);
$request->setContainer(app())->setRedirector(app(Redirector::class));
$request->validateResolved();
app()->instance('request', $request);
return $request;
}
/**
* @param array<string, mixed> $filters
*/
function newsletterSavedList(User $user, array $filters = [], string $name = 'Testliste'): NewsletterList
{
$list = new NewsletterList(['name' => $name, 'created_by' => $user->id]);
$list->applyFilter(NewsletterListFilter::fromArray($filters));
$list->save();
return $list;
}
function newsletterSavedCustomer(string $email): ShoppingUser
{
return ShoppingUser::forceCreate([
'auth_user_id' => 0,
'billing_email' => $email,
'billing_firstname' => 'Erika',
'billing_lastname' => 'Muster',
'billing_salutation' => 'ms',
'billing_country_id' => 1,
'shipping_country_id' => 1,
'language' => 'de',
'subscribed' => 1,
'has_buyed' => 1,
'faker_mail' => 0,
]);
}
it('speichert die aktuelle Filterkombination unter einem Namen', function () {
$admin = newsletterSavedAdmin();
$this->actingAs($admin);
$request = newsletterStoreRequest($admin, ['name' => ' Newsletter DE Kunden ', 'source' => 'both', 'language' => 'de', 'optin' => 'subscribed', 'level_ids' => ['2']]);
$response = (new NewsletterListController(app(NewsletterListService::class)))->store($request);
$list = NewsletterList::first();
expect($list->name)->toBe('Newsletter DE Kunden')
->and($list->source)->toBe('both')
->and($list->created_by)->toBe($admin->id)
->and($list->filters['optin'])->toBe('subscribed')
->and($list->filters['level_ids'])->toBe([2])
->and($list->toFilter()->optin)->toBe('subscribed')
->and($response->getTargetUrl())->toContain('/admin/newsletter-lists/load/'.$list->id);
});
it('verlangt beim Speichern einen Namen', function () {
$admin = newsletterSavedAdmin();
$this->actingAs($admin);
newsletterStoreRequest($admin, ['source' => 'customers']);
})->throws(ValidationException::class);
it('aktualisiert Name und Filter einer gespeicherten Liste', function () {
$admin = newsletterSavedAdmin();
$this->actingAs($admin);
$list = newsletterSavedList($admin, ['source' => 'customers']);
$request = newsletterStoreRequest($admin, ['name' => 'Berater ES', 'source' => 'consultants', 'language' => 'es', 'consultant_status' => 'all']);
(new NewsletterListController(app(NewsletterListService::class)))->update($request, $list);
$list->refresh();
expect($list->name)->toBe('Berater ES')
->and($list->source)->toBe('consultants')
->and($list->filters['language'])->toBe('es')
->and($list->filters['consultant_status'])->toBe('all');
});
it('lädt eine gespeicherte Liste in die Filter-Seite und merkt den Filter', function () {
$admin = newsletterSavedAdmin();
$this->actingAs($admin);
$list = newsletterSavedList($admin, ['source' => 'consultants', 'language' => '']);
$view = (new NewsletterListController(app(NewsletterListService::class)))->load($list);
expect($view->getName())->toBe('admin.newsletter.index')
->and($view->getData()['list']->id)->toBe($list->id)
->and($view->getData()['filter']['source'])->toBe('consultants')
->and($view->getData()['filter']['language'])->toBe('')
->and(get_user_attr('newsletter_list_filter')['source'])->toBe('consultants');
});
it('protokolliert Exporte und aktualisiert die gespeicherte Liste', function () {
Excel::fake();
$admin = newsletterSavedAdmin();
$this->actingAs($admin);
newsletterSavedCustomer('eins@example.com');
newsletterSavedCustomer('zwei@example.com');
$list = newsletterSavedList($admin, ['source' => 'customers', 'language' => 'de']);
$request = NewsletterListFilterRequest::create('/x', 'GET', []);
$request->setUserResolver(fn () => $admin);
(new NewsletterListController(app(NewsletterListService::class)))->exportSaved($request, $list, 'xlsx');
$export = NewsletterListExport::first();
$list->refresh();
expect($export->newsletter_list_id)->toBe($list->id)
->and($export->user_id)->toBe($admin->id)
->and($export->format)->toBe('xlsx')
->and($export->row_count)->toBe(2)
->and($export->filename)->toBe('newsletter_customers_de_'.now()->format('Y-m-d').'.xlsx')
->and($export->filters['source'])->toBe('customers')
->and($export->stats['total'])->toBe(2)
->and($list->last_row_count)->toBe(2)
->and($list->last_exported_at)->not->toBeNull();
Excel::assertDownloaded($export->filename);
});
it('protokolliert Ad-hoc-Exporte ohne Liste und ordnet sie bei list_id der Liste zu', function () {
$admin = newsletterSavedAdmin();
$this->actingAs($admin);
newsletterSavedCustomer('csv@example.com');
$list = newsletterSavedList($admin);
$controller = new NewsletterListController(app(NewsletterListService::class));
$adhoc = NewsletterListFilterRequest::create('/x', 'GET', ['source' => 'customers']);
$adhoc->setUserResolver(fn () => $admin);
$adhoc->setContainer(app())->setRedirector(app(Redirector::class));
$adhoc->validateResolved();
$controller->export($adhoc, 'csv');
$linked = NewsletterListFilterRequest::create('/x', 'GET', ['source' => 'customers', 'list_id' => (string) $list->id]);
$linked->setUserResolver(fn () => $admin);
$linked->setContainer(app())->setRedirector(app(Redirector::class));
$linked->validateResolved();
$controller->export($linked, 'csv');
$exports = NewsletterListExport::orderBy('id')->get();
expect($exports)->toHaveCount(2)
->and($exports[0]->newsletter_list_id)->toBeNull()
->and($exports[0]->format)->toBe('csv')
->and($exports[1]->newsletter_list_id)->toBe($list->id)
->and($list->fresh()->last_row_count)->toBe(1);
});
it('löscht eine Liste und behält das Protokoll', function () {
$admin = newsletterSavedAdmin();
$this->actingAs($admin);
$list = newsletterSavedList($admin);
NewsletterListExport::create([
'newsletter_list_id' => $list->id,
'user_id' => $admin->id,
'format' => 'csv',
'row_count' => 3,
'filename' => 'x.csv',
'filters' => $list->filters,
]);
$response = (new NewsletterListController(app(NewsletterListService::class)))->destroy($list);
expect(NewsletterList::count())->toBe(0)
->and(NewsletterListExport::first()->newsletter_list_id)->toBeNull()
->and($response->getTargetUrl())->toContain('/admin/newsletter-lists/saved');
});
it('liefert gespeicherte Listen und Protokoll als Datatable-JSON', function () {
$admin = newsletterSavedAdmin();
$this->actingAs($admin);
$list = newsletterSavedList($admin, ['source' => 'customers', 'optin' => 'subscribed', 'purchase' => 'months_6'], 'Kunden Opt-in');
NewsletterListExport::create([
'newsletter_list_id' => $list->id,
'user_id' => $admin->id,
'format' => 'xlsx',
'row_count' => 7,
'filename' => 'newsletter_customers_de.xlsx',
'filters' => $list->filters,
'stats' => ['total' => 7],
]);
$request = Request::create('/x', 'GET', ['draw' => 1, 'start' => 0, 'length' => 50]);
$request->setUserResolver(fn () => $admin);
app()->instance('request', $request);
$controller = new NewsletterListController(app(NewsletterListService::class));
$saved = $controller->savedDatatable()->getData(true);
$exports = $controller->exportsDatatable()->getData(true);
expect($saved['recordsTotal'])->toBe(1)
->and($saved['data'][0]['name'])->toContain('Kunden Opt-in')
->and($saved['data'][0]['source'])->toBe('Kunden')
->and($saved['data'][0]['summary'])->toContain('nur mit Opt-in')
->and($saved['data'][0]['summary'])->toContain('6 Monaten')
->and($saved['data'][0]['actions'])->toContain('/admin/newsletter-lists/saved/'.$list->id.'/export/xlsx')
->and($exports['recordsTotal'])->toBe(1)
->and($exports['data'][0]['format'])->toBe('XLSX')
->and($exports['data'][0]['row_count'])->toBe(7)
->and($exports['data'][0]['list'])->toContain('Kunden Opt-in');
});
it('beschreibt einen Filter lesbar', function () {
$service = app(NewsletterListService::class);
$text = $service->describe(NewsletterListFilter::fromArray([
'source' => 'both', 'language' => 'de', 'optin' => 'unsubscribed', 'member_id' => 'none',
'created_from' => '2026-01-01', 'company' => 'with', 'consultant_status' => 'all', 'include_admins' => 1,
]));
expect($text)->toBe('Kunden und Berater · Sprache: DE · Newsletter-Opt-in: nur ohne Opt-in · Zugeordneter Berater / Sponsor: ohne Zuordnung · Angelegt von: 01.01.2026 – … · Firma: nur mit Firma · Berater-Status: alle · Admin-Accounts einbeziehen');
});