diff --git a/app/Enums/ConversionEventType.php b/app/Enums/ConversionEventType.php index 8b549206..0f2505ff 100644 --- a/app/Enums/ConversionEventType.php +++ b/app/Enums/ConversionEventType.php @@ -12,11 +12,21 @@ namespace App\Enums; * Die Wiedereinreichung nach Ablehnung gehört ausdrücklich NICHT hierher. Sie * misst etwas anderes, nämlich ob die Infoseite (1d) wirkt, und wird gegen den * Basiswert aus 0.2 separat geführt (`press-releases:rejection-rate`). + * + * Stufen 2 und 3 seit 13.10.2026 (Roadmap 6.16): Vorher sprang die Leiter vom + * Klick auf IRGENDEINEN Mail-Link direkt zur Einlösung. Ob jemand die + * Verwaltungsseite überhaupt geöffnet und den Link angefordert hatte, war + * nicht zu sehen — und damit nicht, ob es am Hinweis, an der Seite oder an + * der Zustellung hakt. */ enum ConversionEventType: string { case MailLinkClicked = 'mail_link_clicked'; + case ManagePageOpened = 'manage_page_opened'; + + case AccessRequested = 'access_requested'; + case MagicLinkConsumed = 'magic_link_consumed'; case NewsroomEdited = 'newsroom_edited'; @@ -32,10 +42,12 @@ enum ConversionEventType: string { return match ($this) { self::MailLinkClicked => 1, - self::MagicLinkConsumed => 2, - self::NewsroomEdited => 3, - self::AccountCreated => 4, - self::FirstBooking => 5, + self::ManagePageOpened => 2, + self::AccessRequested => 3, + self::MagicLinkConsumed => 4, + self::NewsroomEdited => 5, + self::AccountCreated => 6, + self::FirstBooking => 7, }; } @@ -43,6 +55,8 @@ enum ConversionEventType: string { return match ($this) { self::MailLinkClicked => 'Mail-Link geklickt', + self::ManagePageOpened => 'Verwaltungsseite geöffnet', + self::AccessRequested => 'Anmeldelink angefordert', self::MagicLinkConsumed => 'Magic Link eingelöst', self::NewsroomEdited => 'Newsroom bearbeitet', self::AccountCreated => 'Konto angelegt', @@ -58,7 +72,9 @@ enum ConversionEventType: string { return match ($this) { self::MailLinkClicked => 'Betreff oder Anlass trägt nicht', - self::MagicLinkConsumed => 'Der Verwaltungszugang wird nicht verstanden', + self::ManagePageOpened => 'Der Hinweis auf die Verwaltung wird übersehen', + self::AccessRequested => 'Der Verwaltungszugang wird nicht verstanden', + self::MagicLinkConsumed => 'Der Link kommt nicht an oder wird nicht geöffnet', self::NewsroomEdited => 'Kein Anreiz, das Profil zu pflegen', self::AccountCreated => 'Der Nutzen des Kontos ist nicht erkennbar', self::FirstBooking => 'Preis oder Angebot passt nicht', diff --git a/app/Http/Controllers/Auth/MagicLinkConsumeController.php b/app/Http/Controllers/Auth/MagicLinkConsumeController.php index 24df8dad..4bb74ef2 100644 --- a/app/Http/Controllers/Auth/MagicLinkConsumeController.php +++ b/app/Http/Controllers/Auth/MagicLinkConsumeController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers\Auth; -use App\Enums\ConversionEventType; use App\Http\Controllers\Controller; use App\Models\MagicLink; use App\Models\PressRelease; @@ -75,13 +74,10 @@ class MagicLinkConsumeController extends Controller return redirect()->route('login')->with('status', __('The magic login link has expired or was already used.')); } - // Stufe 2 der Konversionsleiter. Die Herkunft kommt vom Token, nicht + // Stufe 4 der Konversionsleiter. Die Herkunft kommt vom Token, nicht // aus der Session — die ist beim Weg über Mail und Domainwechsel // längst gerissen (Roadmap 0.4). - $this->recorder->record( - ConversionEventType::MagicLinkConsumed, - $this->recorder->sourceOf($magicLink), - ); + $this->recorder->recordMagicLinkConsumed($magicLink); $user = $magicLink->user; @@ -123,10 +119,7 @@ class MagicLinkConsumeController extends Controller ]); if ($claimed > 0) { - $this->recorder->record( - ConversionEventType::MagicLinkConsumed, - $this->recorder->sourceOf($magicLink), - ); + $this->recorder->recordMagicLinkConsumed($magicLink); } } diff --git a/app/Http/Controllers/Web/NewsroomClaimController.php b/app/Http/Controllers/Web/NewsroomClaimController.php index 86a1c1cb..3455c003 100644 --- a/app/Http/Controllers/Web/NewsroomClaimController.php +++ b/app/Http/Controllers/Web/NewsroomClaimController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Web; +use App\Enums\ConversionEventType; use App\Enums\ConversionSource; use App\Http\Controllers\Controller; use App\Models\Company; @@ -9,6 +10,7 @@ use App\Models\PressRelease; use App\Scopes\PortalScope; use App\Services\Company\MergedNewsroomLookup; use App\Services\Company\NewsroomClaimService; +use App\Services\Conversion\ConversionRecorder; use App\Support\WebPortalContext; use Illuminate\Contracts\View\View; use Illuminate\Http\RedirectResponse; @@ -29,9 +31,10 @@ class NewsroomClaimController extends Controller public function __construct( private readonly NewsroomClaimService $claims, private readonly MergedNewsroomLookup $mergedNewsrooms, + private readonly ConversionRecorder $recorder, ) {} - public function show(string $edition, string $slug): View|Response|RedirectResponse + public function show(Request $request, string $edition, string $slug): View|Response|RedirectResponse { $context = WebPortalContext::fromRequest(); $company = $this->claims->findClaimable($context->portalValues, $slug); @@ -40,6 +43,8 @@ class NewsroomClaimController extends Controller return $this->resolveMissing($context, $edition, $slug); } + $this->recorder->recordManagePageOpened($request, $company, $this->sourceOf($request), $this->landingPressRelease($request, $company)); + return view('web.newsroom-claim', [ 'company' => $company, 'recipients' => $this->claims->maskedRecipients($company), @@ -70,7 +75,8 @@ class NewsroomClaimController extends Controller // Enumeration-sicher: dieselbe Meldung, ob ein Konto deaktiviert ist // oder nicht — die Adresse ist ohnehin nur maskiert sichtbar. - $sent = $this->claims->request($company, $request->ip(), $this->sourceOf($request), $this->landingPressReleaseId($request, $company)); + $landing = $this->landingPressRelease($request, $company); + $sent = $this->claims->request($company, $request->ip(), $this->sourceOf($request), $landing?->getKey()); // Der Firmenzähler zählt nur tatsächlich versendete Links — sonst // sperrten Dritte den Inhaber mit Fehlversuchen 24 h aus @@ -78,6 +84,10 @@ class NewsroomClaimController extends Controller // Drossel oben. if ($sent > 0) { RateLimiter::hit($this->companyKey($company->getKey()), 86400); + + // Stufe 3 der Konversionsleiter (Roadmap 6.16): nur, wenn + // wirklich ein Link verschickt wurde. + $this->recorder->record(ConversionEventType::AccessRequested, $this->sourceOf($request), $company, $landing); } return back()->with('newsroom-claim-status', __('static.newsroom_claim.flash_sent')); @@ -88,7 +98,7 @@ class NewsroomClaimController extends Controller * Link), landet er nach dem Login auf genau dieser Meldung samt Aufrufen * (Roadmap 1c.2) — sofern sie wirklich zu dieser Firma gehört. */ - private function landingPressReleaseId(Request $request, Company $company): ?int + private function landingPressRelease(Request $request, Company $company): ?PressRelease { $id = (int) $request->input('pr', 0); @@ -96,7 +106,7 @@ class NewsroomClaimController extends Controller return null; } - return PressRelease::query()->withoutGlobalScope(PortalScope::class)->whereKey($id)->where('company_id', $company->getKey())->exists() ? $id : null; + return PressRelease::query()->withoutGlobalScope(PortalScope::class)->whereKey($id)->where('company_id', $company->getKey())->first(); } private function resolveMissing(WebPortalContext $context, string $edition, string $slug): Response|RedirectResponse diff --git a/app/Http/Middleware/RecordMailLinkClick.php b/app/Http/Middleware/RecordMailLinkClick.php index 681ddb5a..52c26632 100644 --- a/app/Http/Middleware/RecordMailLinkClick.php +++ b/app/Http/Middleware/RecordMailLinkClick.php @@ -7,7 +7,6 @@ use App\Enums\ConversionSource; use App\Models\PressRelease; use App\Scopes\PortalScope; use App\Services\Conversion\ConversionRecorder; -use App\Support\CrawlerIdentifier; use Closure; use Illuminate\Http\Request; use Illuminate\Support\Facades\RateLimiter; @@ -49,7 +48,7 @@ class RecordMailLinkClick // Nur echte Aufrufe. Mailclients und Sicherheitsscanner holen Links // vorab per HEAD oder mit eigener Kennung; das wäre kein Klick, // sondern eine Zustellung (Umsetzungsaudit 29.09.2026, R-08). - if (! $request->isMethod('GET') || $this->isScanner($request->userAgent())) { + if (! $request->isMethod('GET') || $this->recorder->isAutomatedVisit($request->userAgent())) { return $next($request); } @@ -81,23 +80,6 @@ class RecordMailLinkClick return $next($request); } - private function isScanner(?string $userAgent): bool - { - $userAgent = mb_strtolower(trim((string) $userAgent)); - - if ($userAgent === '' || CrawlerIdentifier::identify($userAgent) !== null) { - return true; - } - - foreach ((array) config('conversion.mail_clicks.scanner_agents', []) as $pattern) { - if (str_contains($userAgent, mb_strtolower((string) $pattern))) { - return true; - } - } - - return false; - } - /** * Scope-frei aufgelöst: Der Klick landet je nach Link auf dem Hub oder auf * einem der beiden Spokes, der Portal-Scope würde die Meldung dort diff --git a/app/Listeners/ProcessStripeWebhook.php b/app/Listeners/ProcessStripeWebhook.php index a2f6c499..988d7f16 100644 --- a/app/Listeners/ProcessStripeWebhook.php +++ b/app/Listeners/ProcessStripeWebhook.php @@ -248,7 +248,7 @@ class ProcessStripeWebhook return; } - // Stufe 5 der Konversionsleiter: die erste bezahlte Rechnung eines + // Stufe 7 der Konversionsleiter: die erste bezahlte Rechnung eines // Kontos. Deckt Abos und alle über Rechnung abgewickelten Zahlungen ab; // Einmalkäufe melden zusätzlich in fulfillSinglePurchase(). Die Sperre // je Firma sorgt dafür, dass trotzdem nur die erste Buchung zählt. @@ -469,7 +469,7 @@ class ProcessStripeWebhook if ($result['marked_paid']) { Log::channel('billing')->info('Einmalkauf als bezahlt markiert.', ['single_purchase_id' => $purchase->id]); - // Stufe 5 der Konversionsleiter. recordFirstBooking() zählt je + // Stufe 7 der Konversionsleiter. recordFirstBooking() zählt je // Firma nur den ersten Treffer, deshalb dürfen beide Zahlungspfade // melden, ohne sich gegenseitig zu verfälschen. $this->conversions->recordFirstBooking($purchase->user); diff --git a/app/Listeners/RecordAuthActivity.php b/app/Listeners/RecordAuthActivity.php index 2766c3ff..67170080 100644 --- a/app/Listeners/RecordAuthActivity.php +++ b/app/Listeners/RecordAuthActivity.php @@ -69,7 +69,7 @@ class RecordAuthActivity meta: ['way' => 'form'], ); - // Stufe 4 der Konversionsleiter. Ohne Firmenbezug — beim Anlegen des + // Stufe 6 der Konversionsleiter. Ohne Firmenbezug — beim Anlegen des // Kontos steht noch keine fest; die Zuordnung entsteht erst, wenn ein // Newsroom beansprucht wird. $this->recorder->record(ConversionEventType::AccountCreated, ConversionSource::Organic); diff --git a/app/Services/Conversion/ConversionRecorder.php b/app/Services/Conversion/ConversionRecorder.php index 3d09e1b7..447640ee 100644 --- a/app/Services/Conversion/ConversionRecorder.php +++ b/app/Services/Conversion/ConversionRecorder.php @@ -10,6 +10,9 @@ use App\Models\MagicLink; use App\Models\PressRelease; use App\Models\User; use App\Scopes\PortalScope; +use App\Support\CrawlerIdentifier; +use Illuminate\Http\Request; +use Illuminate\Support\Facades\RateLimiter; /** * Schreibt die Ereignisse der Konversionsleiter (Roadmap 0.5). @@ -73,6 +76,83 @@ class ConversionRecorder $this->record($type, $source, $company, $pressRelease); } + /** + * Stufe 2: die Seite „Diesen Newsroom verwalten“ wurde geöffnet. + * + * Aus einer Mail (`src`) oder über den Hinweis auf der Newsroom- bzw. + * Meldungsseite. Wie beim Mail-Klick zählen nur echte Aufrufe, und + * dieselbe Adresse zählt je Firma höchstens einmal im Zeitfenster. + */ + public function recordManagePageOpened(Request $request, Company $company, ConversionSource $source, ?PressRelease $pressRelease = null): void + { + try { + if (! $request->isMethod('GET') || $this->isAutomatedVisit($request->userAgent())) { + return; + } + + $key = 'manage-page:'.hash('sha256', (string) $request->ip()).':'.$company->getKey(); + $decay = max(1, (int) config('conversion.mail_clicks.throttle_minutes', 30)) * 60; + + if (! RateLimiter::attempt($key, 1, static fn (): bool => true, $decay)) { + return; + } + } catch (\Throwable $e) { + report($e); + + return; + } + + $this->record(ConversionEventType::ManagePageOpened, $source, $company, $pressRelease); + } + + /** + * Stufe 4: Magic Link eingelöst — mit der Firma und Meldung, die das + * Token trägt („Diesen Newsroom verwalten“, Veröffentlichungsmail). + * Ohne diesen Bezug landete jede Einlösung in „ohne Firma“, und die + * Stufe zeigte bis 13.10.2026 immer null Firmen (Roadmap 6.16). + */ + public function recordMagicLinkConsumed(MagicLink $magicLink): void + { + try { + $companyId = (int) ($magicLink->payload['company_id'] ?? 0); + $pressReleaseId = (int) ($magicLink->payload['press_release_id'] ?? 0); + + $company = $companyId > 0 + ? Company::query()->withoutGlobalScope(PortalScope::class)->find($companyId) + : null; + $pressRelease = $pressReleaseId > 0 + ? PressRelease::query()->withoutGlobalScope(PortalScope::class)->find($pressReleaseId) + : null; + } catch (\Throwable $e) { + report($e); + $company = null; + $pressRelease = null; + } + + $this->record(ConversionEventType::MagicLinkConsumed, $this->sourceOf($magicLink), $company, $pressRelease); + } + + /** + * Mailserver-Scanner, Vorschau-Dienste und Crawler — kein Mensch, also + * kein Schritt in der Leiter (Umsetzungsaudit 29.09.2026, R-08). + */ + public function isAutomatedVisit(?string $userAgent): bool + { + $userAgent = mb_strtolower(trim((string) $userAgent)); + + if ($userAgent === '' || CrawlerIdentifier::identify($userAgent) !== null) { + return true; + } + + foreach ((array) config('conversion.mail_clicks.scanner_agents', []) as $pattern) { + if (str_contains($userAgent, mb_strtolower((string) $pattern))) { + return true; + } + } + + return false; + } + /** * Herkunft eines Magic-Link-Tokens. * @@ -102,13 +182,17 @@ class ConversionRecorder } /** - * Stufe 5 der Konversionsleiter. + * Stufe 7 der Konversionsleiter. * * Der Trichter läuft über die FIRMA, gezahlt wird aber vom Konto. Die * Brücke ist die Firma des Kontos — bevorzugt die eigene, sonst die erste * zugeordnete. Ohne Firma bleibt der Bezug leer; das Ereignis ist dann * für die Trichter-Auswertung wertlos, aber immer noch eine ehrliche Zeile * im Log. + * + * Die Herkunft ist wie bei Stufe 5 die des letzten eingelösten Magic + * Links; bis 13.10.2026 stand hier immer `organic`, auch nach einem + * Claim aus der Veröffentlichungsmail (Roadmap 6.16). */ public function recordFirstBooking(?User $user): void { @@ -125,11 +209,11 @@ class ConversionRecorder return; } - $this->recordOnce(ConversionEventType::FirstBooking, ConversionSource::Organic, $company); + $this->recordOnce(ConversionEventType::FirstBooking, $this->lastMagicLinkSource($user), $company); } /** - * Stufe 3 der Konversionsleiter: Newsroom gepflegt (Roadmap 1b.3). + * Stufe 5 der Konversionsleiter: Newsroom gepflegt (Roadmap 1b.3). * * Einmal je Firma. Die Herkunft ist die des letzten eingelösten Magic * Links dieses Kontos — wer über „Diesen Newsroom verwalten" aus der Mail @@ -138,8 +222,17 @@ class ConversionRecorder */ public function recordNewsroomEdited(Company $company, ?User $user): void { + $this->recordOnce(ConversionEventType::NewsroomEdited, $this->lastMagicLinkSource($user), $company); + } + + private function lastMagicLinkSource(?User $user): ConversionSource + { + if ($user === null) { + return ConversionSource::Organic; + } + try { - $source = $user === null ? null : MagicLink::query() + $source = MagicLink::query() ->where('user_id', $user->getKey()) ->whereIn('purpose', MagicLink::LOGIN_PURPOSES) ->whereNotNull('consumed_at') @@ -150,7 +243,7 @@ class ConversionRecorder $source = null; } - $this->recordOnce(ConversionEventType::NewsroomEdited, ConversionSource::parse($source), $company); + return ConversionSource::parse($source); } private function alreadyRecorded(ConversionEventType $type, Company $company): bool diff --git a/resources/views/livewire/customer/press-kits/show.blade.php b/resources/views/livewire/customer/press-kits/show.blade.php index a39d0672..75364ff0 100644 --- a/resources/views/livewire/customer/press-kits/show.blade.php +++ b/resources/views/livewire/customer/press-kits/show.blade.php @@ -192,7 +192,7 @@ new #[Layout('components.layouts.app'), Title('Firma')] class extends Component $this->resetCompanyForm(); - // Stufe 3 der Konversionsleiter: Newsroom gepflegt (einmal je Firma). + // Stufe 5 der Konversionsleiter: Newsroom gepflegt (einmal je Firma). $conversions->recordNewsroomEdited($company, auth()->user()); // Der öffentliche Newsroom zeigt Name, Logo und Beschreibung — die diff --git a/tests/Feature/ConversionFunnelTest.php b/tests/Feature/ConversionFunnelTest.php index 4cd91d6f..c882f3d0 100644 --- a/tests/Feature/ConversionFunnelTest.php +++ b/tests/Feature/ConversionFunnelTest.php @@ -140,7 +140,7 @@ test('consuming a magic link records step two with the token origin', function ( expect($event->source)->toBe(ConversionSource::MailReject); }); -test('registering records step four', function () { +test('registering records the account step', function () { /** @var TestCase $this */ event(new Registered(User::factory()->create())); @@ -166,6 +166,21 @@ test('the first booking is recorded once per company', function () { ->count())->toBe(1); }); +test('the first booking carries the origin of the last redeemed link', function () { + /** @var TestCase $this */ + $user = User::factory()->create(); + $company = Company::factory()->create(['owner_user_id' => $user->id]); + $link = app(MagicLinkGenerator::class)->createLoginLink($user, null, 15, ConversionSource::MailPublish); + $link['magic_link']->forceFill(['consumed_at' => now()])->save(); + + app(ConversionRecorder::class)->recordFirstBooking($user); + + // Bis 13.10.2026 stand hier immer `organic` — auch wer über die + // Veröffentlichungsmail kam und dann buchte (Roadmap 6.16). + expect(ConversionEvent::query()->where('type', ConversionEventType::FirstBooking->value)->sole()->source) + ->toBe(ConversionSource::MailPublish); +}); + test('a broken recording never breaks the request it accompanies', function () { /** @var TestCase $this */ Schema::drop('conversion_events'); @@ -184,7 +199,7 @@ test('the funnel counts distinct companies per step', function () { funnelEvent(ConversionEventType::MailLinkClicked, ConversionSource::MailPublish, $a); funnelEvent(ConversionEventType::MailLinkClicked, ConversionSource::MailPublish, $a); funnelEvent(ConversionEventType::MailLinkClicked, ConversionSource::MailPublish, $b); - funnelEvent(ConversionEventType::MagicLinkConsumed, ConversionSource::MailPublish, $a); + funnelEvent(ConversionEventType::ManagePageOpened, ConversionSource::MailPublish, $a); $data = app(ConversionFunnelReport::class)->build(now()->subMonth()); $steps = collect($data['sources'])->firstWhere('source', ConversionSource::MailPublish->value)['steps']; @@ -264,7 +279,7 @@ test('the ladder is complete and ordered', function () { $steps = array_map(fn (ConversionEventType $t): int => $t->step(), ConversionEventType::ladder()); // Eine Leiter ohne Sprossen sagt nicht, wo es hakt. - expect($steps)->toBe([1, 2, 3, 4, 5]) + expect($steps)->toBe([1, 2, 3, 4, 5, 6, 7]) ->and(ConversionEventType::NewsroomEdited->diagnosis())->toBe('Kein Anreiz, das Profil zu pflegen'); }); diff --git a/tests/Feature/Web/NewsroomClaimTest.php b/tests/Feature/Web/NewsroomClaimTest.php index f8b6ecd3..b9598280 100644 --- a/tests/Feature/Web/NewsroomClaimTest.php +++ b/tests/Feature/Web/NewsroomClaimTest.php @@ -81,8 +81,58 @@ test('requesting the link creates the account, sends the login link with profile $this->get('https://pressekonto.test/magic-login/'.ltrim($plain, '/')) ->assertRedirect(route('me.press-kits.show', ['id' => $company->id], absolute: false)); - expect(ConversionEvent::query()->where('type', ConversionEventType::MagicLinkConsumed->value)->first()->source) - ->toBe(ConversionSource::ProfileClaim); + $consumed = ConversionEvent::query()->where('type', ConversionEventType::MagicLinkConsumed->value)->sole(); + expect($consumed->source)->toBe(ConversionSource::ProfileClaim) + ->and($consumed->company_id)->toBe($company->id); +}); + +test('the way from the publication mail is recorded step by step with the company', function () { + /** @var TestCase $this */ + $company = claimCompany(); + $release = PressRelease::factory()->published()->forPortal(Portal::Businessportal24) + ->create(['company_id' => $company->id, 'published_at' => now()->subDay()]); + $query = '?src='.ConversionSource::MailPublish->value.'&pr='.$release->id; + + // Roadmap 6.16: Vorher sprang die Leiter vom Klick auf irgendeinen + // Mail-Link direkt zur Einlösung, und die Einlösung hatte keine Firma. + $this->get(claimUrl().$query)->assertOk(); + $this->post(claimUrl(), ['src' => ConversionSource::MailPublish->value, 'pr' => $release->id]) + ->assertSessionHas('newsroom-claim-status'); + + $plain = str_replace('/magic-login/', '', parse_url(Mail::queued(MagicLoginLink::class)->first()->loginUrl, PHP_URL_PATH)); + $this->get('https://pressekonto.test/magic-login/'.ltrim($plain, '/')); + + $events = ConversionEvent::query()->get()->keyBy(fn (ConversionEvent $event): string => $event->type->value); + + foreach ([ConversionEventType::ManagePageOpened, ConversionEventType::AccessRequested, ConversionEventType::MagicLinkConsumed] as $type) { + expect($events->get($type->value))->not->toBeNull() + ->and($events[$type->value]->source)->toBe(ConversionSource::MailPublish) + ->and($events[$type->value]->company_id)->toBe($company->id) + ->and($events[$type->value]->press_release_id)->toBe($release->id); + } +}); + +test('opening the page from the newsroom counts as profile claim, once per visitor and never for scanners', function () { + /** @var TestCase $this */ + $company = claimCompany(); + $browser = ['User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 Safari/605.1.15']; + + $this->withHeaders(['User-Agent' => 'Mozilla/5.0 (compatible; Barracuda Sentinel)'])->get(claimUrl())->assertOk(); + $this->withHeaders($browser)->get(claimUrl())->assertOk(); + $this->withHeaders($browser)->get(claimUrl())->assertOk(); + + $opened = ConversionEvent::query()->where('type', ConversionEventType::ManagePageOpened->value)->sole(); + expect($opened->source)->toBe(ConversionSource::ProfileClaim) + ->and($opened->company_id)->toBe($company->id); +}); + +test('a request that sends nothing is not step three', function () { + /** @var TestCase $this */ + claimCompany(['email' => null]); + + $this->post(claimUrl())->assertSessionHas('newsroom-claim-none'); + + expect(ConversionEvent::query()->where('type', ConversionEventType::AccessRequested->value)->exists())->toBeFalse(); }); test('without a company email the link goes to the press contacts', function () {