$code], [ 'phone' => '49', 'en' => $code, 'de' => $code, 'es' => $code, 'fr' => $code, 'it' => $code, 'ru' => $code, ]); } /** * @param array $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 $userAttributes * @param array $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 $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()); });