*/ public function report(string $portal = 'all'): array { $users = $this->candidateUsers($portal); $rows = $users ->map(fn (User $user): array => $this->mapUser($user)) ->values(); return [ 'generated_at' => now()->toIso8601String(), 'portal' => $portal, 'source' => [ 'candidate_rule' => 'users.registration_type = apiuser OR Spatie role api-only', 'eligibility_rule' => 'user is active AND latest legacy invoice status is paid', 'missing_data' => [ 'Legacy api_key values are intentionally not imported.', 'Legacy apiReadAccess/apiWriteAccess assignments are not preserved per-user beyond mapped roles.', 'Users without archived legacy invoices require manual review.', ], ], 'summary' => [ 'total_candidates' => $rows->count(), 'eligible' => $rows->where('classification', 'eligible')->count(), 'needs_review' => $rows->where('classification', 'needs_review')->count(), 'blocked' => $rows->where('classification', 'blocked')->count(), ], 'customers' => $rows->all(), ]; } /** * @return Collection */ private function candidateUsers(string $portal): Collection { return User::query() ->select([ 'id', 'name', 'email', 'portal', 'registration_type', 'is_active', 'is_super_admin', 'legacy_portal', 'legacy_id', 'last_login_at', ]) ->with([ 'roles:id,name', 'legacyInvoices' => fn ($query) => $query ->select([ 'id', 'user_id', 'legacy_portal', 'legacy_id', 'number', 'status', 'invoice_date', 'paid_at', 'total_cents', ]) ->latest('invoice_date') ->latest('id'), ]) ->where(function ($query): void { $query->where('registration_type', RegistrationType::ApiUser->value) ->orWhereHas('roles', fn ($roles) => $roles->where('name', 'api-only')); }) ->when($portal !== 'all', fn ($query) => $query->where('portal', $portal)) ->orderBy('email') ->get(); } /** * @return array */ private function mapUser(User $user): array { $latestInvoice = $user->legacyInvoices->first(); $classification = $this->classification($user, $latestInvoice); return [ 'user_id' => $user->id, 'email' => $user->email, 'name' => $user->name, 'portal' => $user->portal?->value, 'legacy' => [ 'portal' => $user->legacy_portal, 'id' => $user->legacy_id, ], 'registration_type' => $user->registration_type?->value, 'roles' => $user->roles->pluck('name')->values()->all(), 'is_active' => $user->is_active, 'is_super_admin' => $user->is_super_admin, 'last_login_at' => $user->last_login_at?->toDateTimeString(), 'latest_legacy_invoice' => $latestInvoice ? [ 'id' => $latestInvoice->id, 'legacy' => [ 'portal' => $latestInvoice->legacy_portal?->value, 'id' => $latestInvoice->legacy_id, ], 'number' => $latestInvoice->number, 'status' => $latestInvoice->status, 'invoice_date' => $latestInvoice->invoice_date?->toDateString(), 'paid_at' => $latestInvoice->paid_at?->toDateTimeString(), 'total_cents' => $latestInvoice->total_cents, ] : null, 'classification' => $classification, 'recommended_action' => match ($classification) { 'eligible' => 'invite_to_generate_sanctum_token', 'needs_review' => 'manual_billing_review_required', default => 'do_not_grant_api_access', }, ]; } private function classification(User $user, ?LegacyInvoice $latestInvoice): string { if (! $user->is_active) { return 'blocked'; } if ($latestInvoice === null) { return 'needs_review'; } return $latestInvoice->status === 'paid' ? 'eligible' : 'blocked'; } }