*/ public function submitters(int $months = 6): Collection { return User::query() ->withoutGlobalScopes() ->select(['users.id', 'users.name', 'users.email']) ->selectSub( PressRelease::query()->withoutGlobalScopes() ->whereColumn('press_releases.user_id', 'users.id') ->where('press_releases.created_at', '>=', now()->subMonths($months)) ->selectRaw('count(*)'), 'releases' ) ->whereExists(fn ($q) => $q->selectRaw('1') ->from('press_releases as p') ->join('press_release_status_logs as l', 'l.press_release_id', '=', 'p.id') ->whereColumn('p.user_id', 'users.id') ->where('l.source', 'api') ->where('p.created_at', '>=', now()->subMonths($months)) ) ->orderByDesc('releases') ->get() ->map(fn (User $user): SubmitterSummary => new SubmitterSummary( id: (int) $user->id, name: (string) $user->name, email: (string) $user->email, releases: (int) $user->getAttribute('releases'), )); } /** * Adressen dieses Einlieferers, die nicht funktionieren. * * @return Collection */ public function brokenContacts(User $submitter, int $months = 6): Collection { $since = now()->subMonths($months); $companies = $this->companiesOf($submitter, $since); if ($companies->isEmpty()) { return collect(); } $addresses = $this->addressesOf($companies); if ($addresses->isEmpty()) { return collect(); } $emails = $addresses->keys()->all(); $domains = $addresses->keys() ->map(fn (string $email): ?string => PlausibleEmailDomain::domainOf($email)) ->filter() ->unique() ->values(); $suppressions = EmailSuppression::query()->active()->whereIn('email', $emails)->get()->keyBy('email'); $domainChecks = EmailDomainCheck::query()->broken()->whereIn('domain', $domains)->get()->keyBy('domain'); $rows = collect(); foreach ($addresses as $email => $context) { $domain = PlausibleEmailDomain::domainOf($email); $suppression = $suppressions->get($email); $domainCheck = $domain !== null ? $domainChecks->get($domain) : null; if ($suppression === null && $domainCheck === null) { continue; } // Der Domainbefund ist die schärfere Aussage: Er erklärt, warum // die Adresse nicht geht, statt nur festzustellen, dass sie es // nicht tut. $rows->push(new BrokenContact( email: (string) $email, company: $context['company'], companyId: $context['company_id'], fromDomain: $domainCheck !== null, reason: $domainCheck?->state->label() ?? $suppression->reason->label(), detail: $domainCheck->detail ?? $suppression->note ?? null, releases: $context['releases'], lastEvent: $suppression->last_bounced_at ?? $suppression->suppressed_at ?? $domainCheck?->checked_at, )); } return $rows->sortByDesc(fn (BrokenContact $row): int => $row->releases)->values(); } /** * Richtlinien-Ablehnungen: protokolliert, aber nie gesperrt. Für den * Einlieferer trotzdem interessant — sein Empfänger filtert uns weg. * * @return Collection */ public function policyRejections(User $submitter, int $months = 6): Collection { $since = now()->subMonths($months); $companies = $this->companiesOf($submitter, $since); $emails = $this->addressesOf($companies)->keys()->all(); if ($emails === []) { return collect(); } return EmailBounce::query() ->where('kind', EmailBounceKind::Policy->value) ->whereIn('email', $emails) ->where('received_at', '>=', $since) ->orderByDesc('received_at') ->limit(100) ->get(); } /** * Automatisch abgelehnte Meldungen mit Begründung. * * @return Collection */ public function rejectedReleases(User $submitter, int $months = 6): Collection { return collect(DB::table('press_release_status_logs as l') ->join('press_releases as p', 'p.id', '=', 'l.press_release_id') ->leftJoin('companies as c', 'c.id', '=', 'p.company_id') ->where('p.user_id', $submitter->getKey()) ->where('l.source', 'ki') ->where('l.to_status', 'rejected') ->where('l.created_at', '>=', now()->subMonths($months)) ->orderByDesc('l.created_at') ->limit(200) ->get(['p.id', 'p.title', 'c.name as company', 'l.reason', 'l.created_at'])) ->map(fn (object $row): RejectedRelease => new RejectedRelease( id: (int) $row->id, title: (string) $row->title, company: (string) ($row->company ?? '—'), reason: trim((string) $row->reason), rejectedAt: $row->created_at ? Carbon::parse($row->created_at) : null, )); } /** * Firmen, für die dieser Einlieferer im Zeitraum Meldungen gesendet hat, * mit deren Anzahl. * * @return Collection `releases` = Meldungen dieses * Einlieferers für die Firma */ private function companiesOf(User $submitter, Carbon $since): Collection { return Company::query() ->withoutGlobalScopes() ->select(['companies.id', 'companies.name', 'companies.email']) ->selectSub( PressRelease::query()->withoutGlobalScopes() ->whereColumn('press_releases.company_id', 'companies.id') ->where('press_releases.user_id', $submitter->getKey()) ->where('press_releases.created_at', '>=', $since) ->selectRaw('count(*)'), 'releases' ) ->whereExists(fn ($q) => $q->selectRaw('1') ->from('press_releases as p') ->whereColumn('p.company_id', 'companies.id') ->where('p.user_id', $submitter->getKey()) ->where('p.created_at', '>=', $since) ) ->get() ->map(fn (Company $company): SubmitterSummary => new SubmitterSummary( id: (int) $company->id, name: (string) $company->name, email: (string) $company->email, releases: (int) $company->getAttribute('releases'), )); } /** * Alle Adressen dieser Firmen (Firmen-Mail und Pressekontakte), je * Adresse die Firma und die Zahl betroffener Meldungen. * * @param Collection $companies * @return Collection */ private function addressesOf(Collection $companies): Collection { $addresses = collect(); foreach ($companies as $company) { $email = EmailSuppressionService::normalise($company->email); if ($email !== null) { $addresses[$email] = ['company' => $company->name, 'company_id' => $company->id, 'releases' => $company->releases]; } } $contacts = Contact::query() ->withoutGlobalScopes() ->whereIn('company_id', $companies->pluck('id')->all()) ->whereNotNull('email') ->where('email', '!=', '') ->get(['company_id', 'email']); $byCompany = $companies->keyBy('id'); foreach ($contacts as $contact) { $email = EmailSuppressionService::normalise($contact->email); $company = $byCompany->get($contact->company_id); if ($email !== null && $company !== null && ! $addresses->has($email)) { $addresses[$email] = ['company' => $company->name, 'company_id' => $company->id, 'releases' => $company->releases]; } } return $addresses; } }