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>
This commit is contained in:
parent
c568248046
commit
c858bd6ed0
26 changed files with 3664 additions and 0 deletions
153
tests/Feature/Newsletter/NewsletterListControllerTest.php
Normal file
153
tests/Feature/Newsletter/NewsletterListControllerTest.php
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
use App\Exports\xExport;
|
||||
use App\Http\Controllers\Admin\NewsletterListController;
|
||||
use App\Http\Requests\Newsletter\NewsletterListFilterRequest;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Services\Newsletter\NewsletterListService;
|
||||
use App\User;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Routing\Redirector;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function newsletterAdmin(int $admin = 2): User
|
||||
{
|
||||
return User::forceCreate([
|
||||
'email' => 'admin-'.uniqid('', true).'@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'admin' => $admin,
|
||||
'lang' => 'de',
|
||||
'active' => 1,
|
||||
'blocked' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
function newsletterRequest(User $user, array $params = []): NewsletterListFilterRequest
|
||||
{
|
||||
$request = NewsletterListFilterRequest::create('/admin/newsletter-lists/datatable', 'GET', $params);
|
||||
$request->setUserResolver(fn () => $user);
|
||||
$request->setContainer(app())->setRedirector(app(Redirector::class));
|
||||
$request->validateResolved();
|
||||
app()->instance('request', $request);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
function newsletterController(): NewsletterListController
|
||||
{
|
||||
return new NewsletterListController(app(NewsletterListService::class));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
function newsletterListCustomer(array $attributes = []): ShoppingUser
|
||||
{
|
||||
return ShoppingUser::forceCreate(array_merge([
|
||||
'auth_user_id' => 0,
|
||||
'billing_email' => 'kunde-'.uniqid('', true).'@example.com',
|
||||
'billing_firstname' => 'Erika',
|
||||
'billing_lastname' => 'Muster',
|
||||
'billing_salutation' => 'ms',
|
||||
'billing_address' => 'Musterweg 1',
|
||||
'billing_zipcode' => '12345',
|
||||
'billing_city' => 'Musterstadt',
|
||||
'billing_country_id' => 1,
|
||||
'shipping_country_id' => 1,
|
||||
'language' => 'de',
|
||||
'subscribed' => 1,
|
||||
'has_buyed' => 1,
|
||||
'faker_mail' => 0,
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
it('verweigert Nicht-Admins den Zugriff auf die Filter-Anfrage', function () {
|
||||
$this->actingAs(newsletterAdmin(0));
|
||||
|
||||
newsletterRequest(auth()->user(), ['source' => 'customers']);
|
||||
})->throws(AuthorizationException::class);
|
||||
|
||||
it('lehnt ungültige Filterwerte ab', function () {
|
||||
$this->actingAs(newsletterAdmin());
|
||||
|
||||
newsletterRequest(auth()->user(), ['source' => 'alle', 'created_from' => '2026-02-01', 'created_to' => '2026-01-01']);
|
||||
})->throws(ValidationException::class);
|
||||
|
||||
it('liefert die Vorschau als Datatable-JSON mit Kennzahlen', function () {
|
||||
$admin = newsletterAdmin();
|
||||
$this->actingAs($admin);
|
||||
newsletterListCustomer(['billing_email' => 'a@example.com']);
|
||||
newsletterListCustomer(['billing_email' => 'A@example.com']);
|
||||
newsletterListCustomer(['billing_email' => 'b@example.com', 'subscribed' => 0]);
|
||||
|
||||
$request = newsletterRequest($admin, ['source' => 'customers', 'draw' => 1, 'start' => 0, 'length' => 50]);
|
||||
$json = newsletterController()->datatable($request)->getData(true);
|
||||
|
||||
expect($json['recordsTotal'])->toBe(2)
|
||||
->and($json['stats']['total'])->toBe(2)
|
||||
->and($json['stats']['duplicates'])->toBe(1)
|
||||
->and(collect($json['data'])->pluck('email')->all())->toBe(['a@example.com', 'b@example.com'])
|
||||
->and($json['data'][0]['name'])->toBe('Erika Muster')
|
||||
->and($json['data'][0]['optin_label'])->toContain('badge-success')
|
||||
->and($json['data'][1]['optin_label'])->toContain('badge-danger')
|
||||
->and(get_user_attr('newsletter_list_filter')['source'])->toBe('customers');
|
||||
});
|
||||
|
||||
it('exportiert die Liste als CSV mit BOM, Semikolon und Injection-Schutz', function () {
|
||||
$admin = newsletterAdmin();
|
||||
$this->actingAs($admin);
|
||||
newsletterListCustomer(['billing_email' => 'csv@example.com', 'billing_firstname' => '=SUMME(A1)']);
|
||||
|
||||
$response = newsletterController()->export(newsletterRequest($admin, ['source' => 'customers', 'template_only' => 1]), 'csv');
|
||||
|
||||
expect($response)->toBeInstanceOf(StreamedResponse::class);
|
||||
ob_start();
|
||||
$response->sendContent();
|
||||
$content = (string) ob_get_clean();
|
||||
$lines = explode("\n", trim($content));
|
||||
|
||||
expect($content)->toStartWith("\xEF\xBB\xBF")
|
||||
->and($lines[0])->toBe("\xEF\xBB\xBF".'Emailadresse;Vorname;Nachname;Anrede;Straße;PLZ;Wohnort;Firma;Berater')
|
||||
->and($lines[1])->toBe("csv@example.com;'=SUMME(A1);Muster;Frau;\"Musterweg 1\";12345;Musterstadt;;")
|
||||
->and($response->headers->get('content-disposition'))->toContain('newsletter_customers_de_'.now()->format('Y-m-d').'.csv');
|
||||
});
|
||||
|
||||
it('exportiert die Liste als XLSX über die Export-Klasse', function () {
|
||||
Excel::fake();
|
||||
$admin = newsletterAdmin();
|
||||
$this->actingAs($admin);
|
||||
newsletterListCustomer(['billing_email' => 'xlsx@example.com']);
|
||||
|
||||
newsletterController()->export(newsletterRequest($admin, ['source' => 'customers', 'language' => '']), 'xlsx');
|
||||
|
||||
Excel::assertDownloaded('newsletter_customers_alle_'.now()->format('Y-m-d').'.xlsx', function (xExport $export) {
|
||||
$rows = $export->collection();
|
||||
|
||||
return $export->headings()[0][0] === 'Emailadresse'
|
||||
&& count($export->headings()[0]) === 17
|
||||
&& $rows->count() === 1
|
||||
&& $rows->first()[0] === 'xlsx@example.com';
|
||||
});
|
||||
});
|
||||
|
||||
it('zeigt die Index-Seite mit gemerktem Filter', function () {
|
||||
$admin = newsletterAdmin();
|
||||
$this->actingAs($admin);
|
||||
set_user_attr('newsletter_list_filter', ['source' => 'both', 'language' => 'es', 'optin' => 'subscribed']);
|
||||
|
||||
$view = newsletterController()->index(newsletterRequest($admin));
|
||||
|
||||
expect($view->getName())->toBe('admin.newsletter.index')
|
||||
->and($view->getData()['filter']['source'])->toBe('both')
|
||||
->and($view->getData()['filter']['language'])->toBe('es')
|
||||
->and($view->getData()['filter']['optin'])->toBe('subscribed');
|
||||
});
|
||||
256
tests/Feature/Newsletter/NewsletterListSavedTest.php
Normal file
256
tests/Feature/Newsletter/NewsletterListSavedTest.php
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
<?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');
|
||||
});
|
||||
364
tests/Feature/Newsletter/NewsletterListServiceTest.php
Normal file
364
tests/Feature/Newsletter/NewsletterListServiceTest.php
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Country;
|
||||
use App\Models\ShoppingOrder;
|
||||
use App\Models\ShoppingUser;
|
||||
use App\Models\UserAbo;
|
||||
use App\Models\UserAccount;
|
||||
use App\Models\UserLevel;
|
||||
use App\Services\Newsletter\NewsletterListFilter;
|
||||
use App\Services\Newsletter\NewsletterListResult;
|
||||
use App\Services\Newsletter\NewsletterListService;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
uses(TestCase::class, RefreshDatabase::class);
|
||||
|
||||
function newsletterCountry(string $code = 'DE'): Country
|
||||
{
|
||||
return Country::firstOrCreate(['code' => $code], [
|
||||
'phone' => '49', 'en' => $code, 'de' => $code, 'es' => $code, 'fr' => $code, 'it' => $code, 'ru' => $code,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
function newsletterCustomer(array $attributes = []): ShoppingUser
|
||||
{
|
||||
return ShoppingUser::forceCreate(array_merge([
|
||||
'auth_user_id' => 0,
|
||||
'billing_email' => 'kunde-'.uniqid('', true).'@example.com',
|
||||
'billing_firstname' => 'Erika',
|
||||
'billing_lastname' => 'Muster',
|
||||
'billing_salutation' => 'ms',
|
||||
'billing_address' => 'Musterweg 1',
|
||||
'billing_zipcode' => '12345',
|
||||
'billing_city' => 'Musterstadt',
|
||||
'billing_country_id' => newsletterCountry()->id,
|
||||
'shipping_country_id' => 1,
|
||||
'language' => 'de',
|
||||
'subscribed' => 1,
|
||||
'has_buyed' => 1,
|
||||
'faker_mail' => 0,
|
||||
'number' => 'K-'.uniqid('', true),
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $userAttributes
|
||||
* @param array<string, mixed> $accountAttributes
|
||||
*/
|
||||
function newsletterConsultant(array $userAttributes = [], array $accountAttributes = []): User
|
||||
{
|
||||
$account = UserAccount::forceCreate(array_merge([
|
||||
'm_account' => 'B-'.uniqid('', true),
|
||||
'salutation' => 'mr',
|
||||
'first_name' => 'Max',
|
||||
'last_name' => 'Berater',
|
||||
'address' => 'Beraterstraße 2',
|
||||
'zipcode' => '54321',
|
||||
'city' => 'Beraterstadt',
|
||||
'country_id' => newsletterCountry()->id,
|
||||
], $accountAttributes));
|
||||
|
||||
return User::forceCreate(array_merge([
|
||||
'email' => 'berater-'.uniqid('', true).'@example.com',
|
||||
'password' => bcrypt('secret'),
|
||||
'lang' => 'de',
|
||||
'account_id' => $account->id,
|
||||
'active' => 1,
|
||||
'blocked' => 0,
|
||||
'admin' => 0,
|
||||
'test_mode' => 0,
|
||||
], $userAttributes))->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $input
|
||||
*/
|
||||
function newsletterBuild(array $input = []): NewsletterListResult
|
||||
{
|
||||
return app(NewsletterListService::class)->build(NewsletterListFilter::fromArray($input));
|
||||
}
|
||||
|
||||
it('liefert Kunden mit den Vorlage-Spalten und dem zugeordneten Berater', function () {
|
||||
$consultant = newsletterConsultant([], ['first_name' => 'Anna', 'last_name' => 'Sponsor']);
|
||||
$customer = newsletterCustomer(['billing_email' => 'Erika.Muster@Example.com ', 'member_id' => $consultant->id, 'billing_address_2' => 'Hinterhaus']);
|
||||
|
||||
$result = newsletterBuild(['source' => 'customers']);
|
||||
|
||||
expect($result->rows)->toHaveCount(1);
|
||||
$row = $result->rows->first();
|
||||
expect($row['email'])->toBe('erika.muster@example.com')
|
||||
->and($row['firstname'])->toBe('Erika')
|
||||
->and($row['salutation_label'])->toBe('Frau')
|
||||
->and($row['street'])->toBe('Musterweg 1, Hinterhaus')
|
||||
->and($row['consultant'])->toBe('Anna Sponsor')
|
||||
->and($row['consultant_number'])->toBe($consultant->account->m_account)
|
||||
->and($row['country'])->toBe('DE')
|
||||
->and($row['source'])->toBe('customer')
|
||||
->and($row['optin'])->toBe('yes')
|
||||
->and($row['number'])->toBe($customer->number);
|
||||
});
|
||||
|
||||
it('schließt gelöschte, anonymisierte, Fake-, Test- und Berater-Kundendatensätze immer aus', function () {
|
||||
newsletterCustomer(['billing_email' => 'ok@example.com']);
|
||||
newsletterCustomer(['billing_email' => 'faker@example.com', 'faker_mail' => 1]);
|
||||
newsletterCustomer(['billing_email' => 'geloescht_5@dsgvo.invalid']);
|
||||
newsletterCustomer(['billing_email' => 'x-faker@mivita.care']);
|
||||
newsletterCustomer(['billing_email' => 'dev@example.com', 'mode' => 'dev']);
|
||||
newsletterCustomer(['billing_email' => 'prev@example.com', 'mode' => 'prev']);
|
||||
newsletterCustomer(['billing_email' => 'live@example.com', 'mode' => 'live']);
|
||||
newsletterCustomer(['billing_email' => 'berater-shop@example.com', 'auth_user_id' => 99]);
|
||||
newsletterCustomer(['billing_email' => 'userdeleted@example.com', 'user_deleted_at' => now()]);
|
||||
newsletterCustomer(['billing_email' => 'softdeleted@example.com'])->delete();
|
||||
|
||||
$emails = newsletterBuild(['source' => 'customers'])->rows->pluck('email')->all();
|
||||
|
||||
expect($emails)->toEqualCanonicalizing(['ok@example.com', 'live@example.com']);
|
||||
});
|
||||
|
||||
it('schließt gesperrte, gelöschte, Test- und Admin-Berater aus und kann Admins optional einbeziehen', function () {
|
||||
newsletterConsultant(['email' => 'aktiv@example.com']);
|
||||
newsletterConsultant(['email' => 'vip@example.com', 'admin' => 1]);
|
||||
newsletterConsultant(['email' => 'admin@example.com', 'admin' => 2]);
|
||||
newsletterConsultant(['email' => 'sysadmin@example.com', 'admin' => 5]);
|
||||
newsletterConsultant(['email' => 'blocked@example.com', 'blocked' => 1]);
|
||||
newsletterConsultant(['email' => 'testmode@example.com', 'test_mode' => 1]);
|
||||
newsletterConsultant(['email' => 'predeleted@example.com', 'pre_deleted_at' => now()]);
|
||||
newsletterConsultant(['email' => 'delete-7-alt@example.com']);
|
||||
newsletterConsultant(['email' => 'softdeleted@example.com'])->delete();
|
||||
|
||||
$default = newsletterBuild(['source' => 'consultants'])->rows->pluck('email')->all();
|
||||
$withAdmins = newsletterBuild(['source' => 'consultants', 'include_admins' => 1])->rows->pluck('email')->all();
|
||||
|
||||
expect($default)->toEqualCanonicalizing(['aktiv@example.com', 'vip@example.com'])
|
||||
->and($withAdmins)->toEqualCanonicalizing(['aktiv@example.com', 'vip@example.com', 'admin@example.com']);
|
||||
});
|
||||
|
||||
it('entfernt Duplikate über beide Quellen, Berater gewinnt vor Kunde und Opt-in vor ohne Opt-in', function () {
|
||||
newsletterConsultant(['email' => 'Doppelt@Example.com']);
|
||||
newsletterCustomer(['billing_email' => 'doppelt@example.com', 'billing_firstname' => 'Kunde']);
|
||||
newsletterCustomer(['billing_email' => 'zweimal@example.com', 'billing_firstname' => 'Ohne', 'subscribed' => 0]);
|
||||
newsletterCustomer(['billing_email' => 'ZWEIMAL@example.com', 'billing_firstname' => 'Mit', 'subscribed' => 1]);
|
||||
newsletterCustomer(['billing_email' => 'zweimal@example.com', 'billing_firstname' => 'Ohne2', 'subscribed' => 0]);
|
||||
|
||||
$result = newsletterBuild(['source' => 'both', 'consultant_status' => 'all']);
|
||||
$byEmail = $result->rows->keyBy('email');
|
||||
|
||||
expect($result->rows)->toHaveCount(2)
|
||||
->and($byEmail['doppelt@example.com']['source'])->toBe('consultant')
|
||||
->and($byEmail['zweimal@example.com']['firstname'])->toBe('Mit')
|
||||
->and($result->stats['duplicates'])->toBe(3)
|
||||
->and($result->stats['total'])->toBe(2)
|
||||
->and($result->stats['customers'])->toBe(1)
|
||||
->and($result->stats['consultants'])->toBe(1);
|
||||
});
|
||||
|
||||
it('verwirft ungültige E-Mail-Adressen und zählt sie', function () {
|
||||
newsletterCustomer(['billing_email' => 'kontakt@ kaputt.de']);
|
||||
newsletterCustomer(['billing_email' => '#']);
|
||||
newsletterCustomer(['billing_email' => 'gut@example.com']);
|
||||
|
||||
$result = newsletterBuild(['source' => 'customers']);
|
||||
|
||||
expect($result->rows->pluck('email')->all())->toBe(['gut@example.com'])
|
||||
->and($result->stats['invalid'])->toBe(2);
|
||||
});
|
||||
|
||||
it('filtert nach Sprache und behandelt fehlende Sprache wie Deutsch', function () {
|
||||
newsletterCustomer(['billing_email' => 'de@example.com', 'language' => 'de']);
|
||||
newsletterCustomer(['billing_email' => 'null@example.com', 'language' => null]);
|
||||
newsletterCustomer(['billing_email' => 'es@example.com', 'language' => 'es']);
|
||||
newsletterConsultant(['email' => 'berater-es@example.com', 'lang' => 'es']);
|
||||
|
||||
expect(newsletterBuild(['source' => 'both', 'language' => 'de'])->rows->pluck('email')->all())
|
||||
->toEqualCanonicalizing(['de@example.com', 'null@example.com'])
|
||||
->and(newsletterBuild(['source' => 'both', 'language' => 'es'])->rows->pluck('email')->all())
|
||||
->toEqualCanonicalizing(['es@example.com', 'berater-es@example.com'])
|
||||
->and(newsletterBuild(['source' => 'both', 'language' => ''])->rows)->toHaveCount(4);
|
||||
});
|
||||
|
||||
it('filtert nach Land bei Kunden und Beratern', function () {
|
||||
$at = newsletterCountry('AT');
|
||||
newsletterCustomer(['billing_email' => 'de@example.com']);
|
||||
newsletterCustomer(['billing_email' => 'at@example.com', 'billing_country_id' => $at->id]);
|
||||
newsletterConsultant(['email' => 'berater-at@example.com'], ['country_id' => $at->id]);
|
||||
newsletterConsultant(['email' => 'berater-de@example.com']);
|
||||
|
||||
$emails = newsletterBuild(['source' => 'both', 'country_ids' => [$at->id]])->rows->pluck('email')->all();
|
||||
|
||||
expect($emails)->toEqualCanonicalizing(['at@example.com', 'berater-at@example.com']);
|
||||
});
|
||||
|
||||
it('filtert nach Opt-in', function () {
|
||||
newsletterCustomer(['billing_email' => 'ja@example.com', 'subscribed' => 1]);
|
||||
newsletterCustomer(['billing_email' => 'nein@example.com', 'subscribed' => 0]);
|
||||
|
||||
expect(newsletterBuild(['optin' => 'subscribed'])->rows->pluck('email')->all())->toBe(['ja@example.com'])
|
||||
->and(newsletterBuild(['optin' => 'unsubscribed'])->rows->pluck('email')->all())->toBe(['nein@example.com'])
|
||||
->and(newsletterBuild(['optin' => 'all'])->rows)->toHaveCount(2);
|
||||
});
|
||||
|
||||
it('filtert nach Kaufstatus inklusive Zeitraum und aktivem Abo', function () {
|
||||
$country = newsletterCountry();
|
||||
$never = newsletterCustomer(['billing_email' => 'nie@example.com', 'has_buyed' => 0]);
|
||||
$recent = newsletterCustomer(['billing_email' => 'kuerzlich@example.com']);
|
||||
$old = newsletterCustomer(['billing_email' => 'alt@example.com']);
|
||||
$abo = newsletterCustomer(['billing_email' => 'abo@example.com']);
|
||||
|
||||
foreach ([[$recent, now()->subMonths(2)], [$old, now()->subMonths(4)]] as [$shoppingUser, $createdAt]) {
|
||||
ShoppingOrder::forceCreate([
|
||||
'shopping_user_id' => $shoppingUser->id,
|
||||
'auth_user_id' => 0,
|
||||
'country_id' => $country->id,
|
||||
'language' => 'de',
|
||||
'user_shop_id' => 1,
|
||||
'payment_for' => 6,
|
||||
'total' => 50,
|
||||
'subtotal' => 50,
|
||||
'points' => 25,
|
||||
'paid' => true,
|
||||
'shipped' => 0,
|
||||
'mode' => 'live',
|
||||
'created_at' => $createdAt,
|
||||
'updated_at' => $createdAt,
|
||||
]);
|
||||
}
|
||||
|
||||
UserAbo::create([
|
||||
'user_id' => 1,
|
||||
'shopping_user_id' => $abo->id,
|
||||
'is_for' => 'customer',
|
||||
'email' => $abo->billing_email,
|
||||
'payone_userid' => 1,
|
||||
'clearingtype' => 'cc',
|
||||
'active' => true,
|
||||
'status' => 2,
|
||||
'abo_interval' => 5,
|
||||
'next_date' => now()->addDays(3),
|
||||
]);
|
||||
UserAbo::create([
|
||||
'user_id' => 1,
|
||||
'shopping_user_id' => $never->id,
|
||||
'is_for' => 'customer',
|
||||
'email' => $never->billing_email,
|
||||
'payone_userid' => 2,
|
||||
'clearingtype' => 'cc',
|
||||
'active' => false,
|
||||
'status' => 4,
|
||||
'abo_interval' => 5,
|
||||
'next_date' => now()->addDays(3),
|
||||
]);
|
||||
|
||||
$emails = fn (string $purchase) => newsletterBuild(['purchase' => $purchase])->rows->pluck('email')->all();
|
||||
|
||||
expect($emails('has_buyed'))->toEqualCanonicalizing(['kuerzlich@example.com', 'alt@example.com', 'abo@example.com'])
|
||||
->and($emails('none'))->toBe(['nie@example.com'])
|
||||
->and($emails('months_3'))->toBe(['kuerzlich@example.com'])
|
||||
->and($emails('months_6'))->toEqualCanonicalizing(['kuerzlich@example.com', 'alt@example.com'])
|
||||
->and($emails('active_abo'))->toBe(['abo@example.com']);
|
||||
});
|
||||
|
||||
it('filtert nach zugeordnetem Berater bzw. Sponsor', function () {
|
||||
$sponsor = newsletterConsultant(['email' => 'sponsor@example.com']);
|
||||
newsletterCustomer(['billing_email' => 'zugeordnet@example.com', 'member_id' => $sponsor->id]);
|
||||
newsletterCustomer(['billing_email' => 'ohne@example.com', 'member_id' => null]);
|
||||
newsletterConsultant(['email' => 'firstline@example.com', 'm_sponsor' => $sponsor->id]);
|
||||
|
||||
expect(newsletterBuild(['source' => 'both', 'member_id' => (string) $sponsor->id])->rows->pluck('email')->all())
|
||||
->toEqualCanonicalizing(['zugeordnet@example.com', 'firstline@example.com'])
|
||||
->and(newsletterBuild(['source' => 'customers', 'member_id' => 'none'])->rows->pluck('email')->all())
|
||||
->toBe(['ohne@example.com']);
|
||||
});
|
||||
|
||||
it('filtert nach Anlagedatum und Firma', function () {
|
||||
newsletterCustomer(['billing_email' => 'alt@example.com', 'billing_company' => 'Firma GmbH', 'created_at' => '2025-01-15 10:00:00']);
|
||||
newsletterCustomer(['billing_email' => 'neu@example.com', 'billing_company' => '', 'created_at' => '2026-03-01 10:00:00']);
|
||||
newsletterConsultant(['email' => 'berater-firma@example.com'], ['company' => 'Beratung UG']);
|
||||
newsletterConsultant(['email' => 'berater-privat@example.com'], ['company' => null]);
|
||||
|
||||
expect(newsletterBuild(['created_from' => '2026-01-01', 'created_to' => '2026-12-31'])->rows->pluck('email')->all())
|
||||
->toBe(['neu@example.com'])
|
||||
->and(newsletterBuild(['source' => 'both', 'company' => 'with'])->rows->pluck('email')->all())
|
||||
->toEqualCanonicalizing(['alt@example.com', 'berater-firma@example.com'])
|
||||
->and(newsletterBuild(['source' => 'both', 'company' => 'without'])->rows->pluck('email')->all())
|
||||
->toEqualCanonicalizing(['neu@example.com', 'berater-privat@example.com']);
|
||||
});
|
||||
|
||||
it('filtert Berater nach Status und Level und gibt den Status in der Zeile aus', function () {
|
||||
$level = UserLevel::forceCreate(['name' => 'Starter', 'pos' => 1]);
|
||||
newsletterConsultant(['email' => 'aktiv@example.com', 'm_level' => $level->id]);
|
||||
newsletterConsultant(['email' => 'inaktiv@example.com', 'active' => 0]);
|
||||
newsletterConsultant(['email' => 'anderes-level@example.com']);
|
||||
|
||||
$active = newsletterBuild(['source' => 'consultants', 'consultant_status' => 'active']);
|
||||
$inactive = newsletterBuild(['source' => 'consultants', 'consultant_status' => 'inactive']);
|
||||
$all = newsletterBuild(['source' => 'consultants', 'consultant_status' => 'all']);
|
||||
$byLevel = newsletterBuild(['source' => 'consultants', 'consultant_status' => 'all', 'level_ids' => [$level->id]]);
|
||||
|
||||
expect($active->rows->pluck('email')->all())->toEqualCanonicalizing(['aktiv@example.com', 'anderes-level@example.com'])
|
||||
->and($inactive->rows->pluck('email')->all())->toBe(['inaktiv@example.com'])
|
||||
->and($inactive->rows->first()['status'])->toBe('inactive')
|
||||
->and($inactive->rows->first()['optin'])->toBe('contract')
|
||||
->and($all->rows)->toHaveCount(3)
|
||||
->and($byLevel->rows->pluck('email')->all())->toBe(['aktiv@example.com'])
|
||||
->and($byLevel->rows->first()['level'])->toBe('Starter');
|
||||
});
|
||||
|
||||
it('liefert Export-Kopfzeile und Zeilen in Vorlagen-Reihenfolge, optional nur die neun Vorlage-Spalten', function () {
|
||||
$consultant = newsletterConsultant([], ['first_name' => 'Anna', 'last_name' => 'Sponsor']);
|
||||
newsletterCustomer([
|
||||
'billing_email' => 'export@example.com',
|
||||
'billing_firstname' => 'Erika',
|
||||
'billing_lastname' => 'Muster',
|
||||
'billing_company' => 'Muster GmbH',
|
||||
'member_id' => $consultant->id,
|
||||
]);
|
||||
$service = app(NewsletterListService::class);
|
||||
$rows = newsletterBuild()->rows;
|
||||
|
||||
$fullHeaders = $service->exportHeaders(false);
|
||||
$templateHeaders = $service->exportHeaders(true);
|
||||
$templateRows = $service->exportRows($rows, true);
|
||||
$fullRows = $service->exportRows($rows, false);
|
||||
|
||||
expect($templateHeaders)->toBe(['Emailadresse', 'Vorname', 'Nachname', 'Anrede', 'Straße', 'PLZ', 'Wohnort', 'Firma', 'Berater'])
|
||||
->and($fullHeaders)->toHaveCount(17)
|
||||
->and($fullHeaders[9])->toBe('Land')
|
||||
->and($templateRows[0])->toBe(['export@example.com', 'Erika', 'Muster', 'Frau', 'Musterweg 1', '12345', 'Musterstadt', 'Muster GmbH', 'Anna Sponsor'])
|
||||
->and($fullRows[0])->toHaveCount(17)
|
||||
->and($fullRows[0][9])->toBe('DE')
|
||||
->and($fullRows[0][11])->toBe('Kunde')
|
||||
->and($fullRows[0][14])->toBe('ja');
|
||||
});
|
||||
|
||||
it('baut den Filter mit Defaults und akzeptiert nur erlaubte Werte', function () {
|
||||
$default = NewsletterListFilter::fromArray([]);
|
||||
$custom = NewsletterListFilter::fromArray([
|
||||
'source' => 'unsinn',
|
||||
'language' => '',
|
||||
'country_ids' => ['3', '', '3', 'x'],
|
||||
'purchase' => 'months_6',
|
||||
'member_id' => '12',
|
||||
'created_from' => '2026-01-01',
|
||||
'created_to' => 'kein datum',
|
||||
'include_admins' => '1',
|
||||
]);
|
||||
|
||||
expect($default->source)->toBe('customers')
|
||||
->and($default->language)->toBe('de')
|
||||
->and($default->optin)->toBe('all')
|
||||
->and($default->consultantStatus)->toBe('active')
|
||||
->and($custom->source)->toBe('customers')
|
||||
->and($custom->language)->toBeNull()
|
||||
->and($custom->countryIds)->toBe([3, 0])
|
||||
->and($custom->purchaseMonths())->toBe(6)
|
||||
->and($custom->memberIdValue())->toBe(12)
|
||||
->and($custom->createdFrom?->format('Y-m-d H:i:s'))->toBe('2026-01-01 00:00:00')
|
||||
->and($custom->createdTo)->toBeNull()
|
||||
->and($custom->includeAdmins)->toBeTrue()
|
||||
->and($default->cacheKey())->not->toBe($custom->cacheKey());
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue