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>
256 lines
10 KiB
PHP
256 lines
10 KiB
PHP
<?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');
|
||
});
|