, * firstline_count: int, * downline_count: int, * pre_sponsor_count: int, * customers_count: int, * customers_deleted_count: int, * abos_count: int, * abos_active_count: int, * portal_customers_count: int * } */ public function preview(User $source): array { $firstline = $this->firstlineQuery($source)->with('account')->orderBy('email')->get(); $customers = ShoppingUser::withTrashed()->where('member_id', $source->id); $abos = UserAbo::withTrashed()->where('member_id', $source->id); return [ 'firstline' => $firstline, 'firstline_count' => $firstline->count(), 'downline_count' => count($this->downlineUserIds($source)), 'pre_sponsor_count' => $this->preSponsorQuery($source)->count(), 'customers_count' => (clone $customers)->count(), 'customers_deleted_count' => (clone $customers)->whereNotNull('deleted_at')->count(), 'abos_count' => (clone $abos)->count(), 'abos_active_count' => (clone $abos)->where('active', true)->count(), 'portal_customers_count' => Customer::where('member_id', $source->id)->count(), ]; } /** * Prüft, ob eine Übertragung von $source auf $target zulässig ist. * * Ein Zielberater unterhalb der Firstline der Quelle würde einen Zyklus * erzeugen (seine eigene Upline würde unter ihn gehängt) und wird abgelehnt. * Ein Zielberater direkt in der Firstline ist erlaubt: Er behält seinen * eigenen Sponsor und bekommt die übrigen Firstline-Berater als Downline. * * @throws ConsultantTransferException */ public function assertTransferable(User $source, User $target): void { if ($source->id === $target->id) { throw new ConsultantTransferException(__('msg.transfer_error_same_user')); } if ($target->deleted_at || ! $target->active || $target->blocked || ! $target->isActiveAccount()) { throw new ConsultantTransferException(__('msg.transfer_error_target_inactive')); } $isDirectChild = (int) $target->m_sponsor === (int) $source->id; if (! $isDirectChild && in_array($target->id, $this->downlineUserIds($source), true)) { throw new ConsultantTransferException(__('msg.transfer_error_target_in_downline')); } } /** * Übertragung ausführen. * * @return array Zusammenfassung der bewegten Datensätze * * @throws ConsultantTransferException */ public function transfer(User $source, User $target, ?int $executedByUserId = null): array { $this->assertTransferable($source, $target); $summary = DB::transaction(function () use ($source, $target): array { return [ 'firstline' => $this->transferFirstline($source, $target), 'pre_sponsor' => $this->transferPreSponsor($source, $target), 'customers' => $this->transferCustomers($source, $target), 'abos' => UserAbo::withTrashed()->where('member_id', $source->id)->update(['member_id' => $target->id]), 'portal_customers' => Customer::where('member_id', $source->id)->update(['member_id' => $target->id]), ]; }); $this->logTransfer($source, $target, $executedByUserId, $summary); return $summary; } /** * IDs aller Berater unterhalb von $source (alle Ebenen, inkl. soft-deleted). * * @return array */ public function downlineUserIds(User $source): array { $ids = []; $visited = [$source->id => true]; $frontier = [$source->id]; while ($frontier !== []) { $children = User::withTrashed() ->whereIn('m_sponsor', $frontier) ->whereColumn('id', '!=', 'm_sponsor') ->pluck('id'); $frontier = []; foreach ($children as $childId) { $childId = (int) $childId; if (isset($visited[$childId])) { continue; } $visited[$childId] = true; $ids[] = $childId; $frontier[] = $childId; } } return $ids; } protected function transferFirstline(User $source, User $target): int { $count = 0; foreach ($this->firstlineQuery($source)->where('id', '!=', $target->id)->get() as $child) { UserCleanUpLog::create([ 'inactive_sponsor_id' => $source->id, 'child_user_id' => $child->id, 'new_sponsor_id' => $target->id, ]); $child->m_sponsor = $target->id; $child->save(); $count++; } return $count; } protected function transferPreSponsor(User $source, User $target): int { $count = 0; foreach ($this->preSponsorQuery($source)->where('id', '!=', $target->id)->get() as $parked) { UserCleanUpLog::create([ 'inactive_sponsor_id' => $source->id, 'child_user_id' => $parked->id, 'new_sponsor_id' => $target->id, ]); $parked->pre_sponsor = $target->id; $parked->save(); $count++; } return $count; } protected function transferCustomers(User $source, User $target): int { $count = 0; foreach (ShoppingUser::withTrashed()->where('member_id', $source->id)->get() as $customer) { ShoppingUserMemberLog::create([ 'pre_member_id' => $source->id, 'shopping_user_id' => $customer->id, 'new_member_id' => $target->id, ]); $customer->member_id = $target->id; $customer->save(); $count++; } return $count; } /** * @return Builder */ protected function firstlineQuery(User $source) { return User::withTrashed() ->where('m_sponsor', $source->id) ->where('id', '!=', $source->id); } /** * @return Builder */ protected function preSponsorQuery(User $source) { return User::withTrashed() ->where('pre_sponsor', $source->id) ->where('id', '!=', $source->id); } /** * @param array $summary */ protected function logTransfer(User $source, User $target, ?int $executedByUserId, array $summary): void { $message = 'Berater-Stamm übertragen von '.$source->email.' (#'.$source->id.') an '.$target->email.' (#'.$target->id.'). Umfang: '.json_encode($summary); SysLog::action(self::SYSLOG_ACTION, 'cleanup', 3) ->setUserId($executedByUserId) ->setModel($source->id, User::class) ->setMessage($message) ->save(); Log::channel('cleanup')->info(self::SYSLOG_ACTION.': '.$message.' | by_admin='.($executedByUserId ?? 'system')); } }