Statistik-Änderungen 2026-08-19 (dev/2026-07-22/entwicklungsplan-2026-08-19.md, alle 8 Pakete gemäß beantworteten F-Fragen): - N1 Monatsstatistik: Team-Cluster (Firstlines klickbar -> neue Seite user_team_firstlines, Teampartner gesamt/Payline [nur aktive], Abos Payline mit +N) + Level-Kachel (Quali-Stufe, fehlende Payline-/persönliche Punkte, Karriere-Level); LiveCommissionService-Snapshot v3 reicht Level-/Quali-Daten durch - N2 Abo-Kacheln auf Home monatsbezogen (aboStockCount-Rückrechnung über cancel/deleted/Status-Historie; VIP-Statistik bleibt tagesaktuell) - N3 VIP-Statistik 'Recruiter des Monats' (Neupartner-Definition wie KPI, rekursives Ketten-Volumen als Gesamtpunkte inkl. Starterpaket) + Drilldown/CSV-Spalte 'Punkte gesamt' - N4 Prognose-Block 'Level & Provision laut Prognose (Stand 25.)' mit unverbindlich-Kennzeichnung (ebenengenaue offene Abo-Punkte + TP-Laufstände) - N5 Checkout-Herkunft: Dropdown raus, Pflicht-Freitext (lokalisierte Fehlermeldung) - N6 Berater-Profilfoto (user_accounts.profile_image, Einwilligung als Sprachkey-Platzhalter, DSGVO-Löschung) + teaminterne Top-3-Karte (TopResultsService, Scope-Schalter) - N7 USt-Optionen: 'nicht umsatzsteuerpflichtig' zuerst + '§ 19 UStG'-Textfix - N8 Jahresgebühr-Nachfassliste je Berater (AnnualFeeDueService, überfällig/1-3 Monate, Sidenav) Kontroll-Review (8 Perspektiven) angewendet: next-Level-Fallback im Steady-State (resolveNextLevel — sonst 'Höchstes Level erreicht' für Normalfälle), _calculated-Punktebasis bei mehr Paylines, keine Wiederbelebung deaktivierter Abos in der Monatsrückrechnung, Payline-Abo-Kachel monatskonsistent, Top-3-Nachrücker; Performance: BackofficeDashboardService als Singleton mit lineBuckets-Memo (+flushLineBucketsMemo), level_forecast 6h-Cache; Reuse: User-Scopes firstlinesOf/newPartnerIn, SalesVolumePoints::totalPointsByUser, Slim::storeCroppedImage (3 Upload-Kopien vereinigt), activeAbo-Scope statt Inline-Prädikate, Form::open-Konvention, Return-Types Repair-Härtung (Kundenentscheide): VP-Nachtrepair dedupliziert über shopping_order_id (Live-Pfad schreibt sie jetzt mit), zählende Alt-Zeilen-Erkennung, Vormonats-Scan für Monatswechsel, Firmenaccount-Buchungen (config mivita.company_user_id) werden automatisch auf den Berater umgebucht (in place bzw. per Gegenbuchung bei geschlossenem Monat; Echtfall Anna Schall Order 58797 -> Christina Schuder), Berater-zu-Berater nur Warnung; Incentive-Repair: m_sponsor=NULL löscht nicht mehr, FK-Repair vor Stale-Bereinigung, gebatchte Lookups, zentrales Incentive::qualificationWindow(); .env aus dem Git-Tracking entfernt (Pflege per SFTP) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
470 lines
18 KiB
PHP
470 lines
18 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Abo;
|
||
|
||
use App\Models\UserAbo;
|
||
use App\Models\UserSalesVolume;
|
||
use App\Services\Backoffice\BackofficeDashboardService;
|
||
use App\Services\Backoffice\SalesVolumePoints;
|
||
use App\Services\BusinessPlan\LiveCommissionService;
|
||
use App\User;
|
||
use Carbon\Carbon;
|
||
use Illuminate\Database\Eloquent\Builder;
|
||
use Illuminate\Support\Facades\Cache;
|
||
|
||
/**
|
||
* Payline-bezogene Abo-Kennzahlen (Konzept B8 + B10):
|
||
*
|
||
* - Payline = die ersten N Ebenen der Downline; N ("paylines") kommt aus dem
|
||
* Provisionslevel des Users (Live-TreeCalcBot bzw. UserBusiness-Snapshot).
|
||
* - B8 aboTurnover(): Abo-Gesamtumsatz der Payline als Soll (Abo-Punkte-Bestand
|
||
* pro Zyklus) und Ist (im Monat abgerechnete Abo-Punkte, F10).
|
||
* - B10 forecastDays(): voraussichtlicher Payline-Gesamtumsatz zu den festen
|
||
* Ausführungsterminen 5./10./20./25. — Basis (Kundenumsatz + Payline-Punkte
|
||
* Stand heute) plus die bis zum jeweiligen Termin noch anstehenden
|
||
* Abo-Punkte, kumuliert (F11).
|
||
*/
|
||
class PaylineForecastService
|
||
{
|
||
/**
|
||
* F12 (dokumentierte Annahme, Kunde entscheidet nach Umsetzung): Die eigenen
|
||
* Abos und eigenen Kundenabos des Beraters zählen in der Prognose MIT, weil
|
||
* die Basis der Formel den aktuellen Kundenumsatz enthält und kommende
|
||
* eigene Ausführungen genau diesen Anteil erhöhen. Über diesen Schalter
|
||
* ohne Umbau umkehrbar.
|
||
*/
|
||
public const INCLUDE_OWN_ABOS = true;
|
||
|
||
private const CACHE_HOURS = 6;
|
||
|
||
public function __construct(
|
||
private BackofficeDashboardService $dashboardService,
|
||
private LiveCommissionService $commissionService,
|
||
) {}
|
||
|
||
/**
|
||
* Payline-Kontext (Anzahl bezahlter Ebenen + Mitglieder-IDs), 6 h gecacht —
|
||
* die Payline-Tiefe kommt aus dem gemeinsamen Live-Lauf, die Abo-Summen
|
||
* bleiben live. Ein fehlgeschlagener Live-Lauf wird nicht gecacht, damit
|
||
* ein transienter Fehler die Karte nicht 6 h lang leert.
|
||
*
|
||
* @return array{paylines: int, member_ids: int[]}
|
||
*/
|
||
public function paylineContext(User $user, int $month, int $year): array
|
||
{
|
||
$cacheKey = "payline_context_{$user->id}_{$month}_{$year}";
|
||
$cached = Cache::get($cacheKey);
|
||
if (is_array($cached)) {
|
||
return $cached;
|
||
}
|
||
|
||
[$context, $failed] = $this->buildPaylineContext($user, $month, $year);
|
||
|
||
if (! $failed) {
|
||
Cache::put($cacheKey, $context, now()->addHours(self::CACHE_HOURS));
|
||
}
|
||
|
||
return $context;
|
||
}
|
||
|
||
/**
|
||
* B8: Abo-Gesamtumsatz in der Payline — Soll und Ist, plus die Anzahl der
|
||
* dahinterstehenden Abos (Berater- und Kundenabos, Kundenwunsch 2026-07-24).
|
||
*
|
||
* @return array{paylines: int, members: int, target: float, actual: float, team_abo_count: int, team_customer_abo_count: int}
|
||
*/
|
||
public function aboTurnover(User $user, int $month, int $year): array
|
||
{
|
||
$context = $this->paylineContext($user, $month, $year);
|
||
$memberIds = $context['member_ids'];
|
||
|
||
return [
|
||
'paylines' => $context['paylines'],
|
||
'members' => count($memberIds),
|
||
'target' => $this->aboTargetPoints($memberIds),
|
||
'actual' => $this->aboActualPoints($memberIds, $month, $year),
|
||
'team_abo_count' => $this->aboCount($memberIds, 'me', 'user_id'),
|
||
'team_customer_abo_count' => $this->aboCount($memberIds, 'ot', 'member_id'),
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Neue Abos der Payline-Mitglieder im Monat (Berater- + Kundenabos,
|
||
* start_date im Monat) — für die "Neue Abos in der Payline"-Kachel (N1).
|
||
*/
|
||
public function newPaylineAboCount(User $user, int $month, int $year): int
|
||
{
|
||
$memberIds = $this->paylineContext($user, $month, $year)['member_ids'];
|
||
|
||
if ($memberIds === []) {
|
||
return 0;
|
||
}
|
||
|
||
$startDate = Carbon::create($year, $month, 1)->startOfMonth();
|
||
$endDate = $startDate->copy()->endOfMonth();
|
||
|
||
return UserAbo::query()
|
||
->activeAbo()
|
||
->whereBetween('start_date', [$startDate, $endDate])
|
||
->where(function (Builder $query) use ($memberIds) {
|
||
$query->where(fn (Builder $inner) => $inner->where('is_for', 'me')->whereIn('user_id', $memberIds))
|
||
->orWhere(fn (Builder $inner) => $inner->where('is_for', 'ot')->whereIn('member_id', $memberIds));
|
||
})
|
||
->count();
|
||
}
|
||
|
||
/**
|
||
* Anzahl aktiver Abos der Payline-Mitglieder (gleiche Filter wie Soll).
|
||
*
|
||
* @param int[] $memberIds
|
||
*/
|
||
private function aboCount(array $memberIds, string $isFor, string $userColumn): int
|
||
{
|
||
if ($memberIds === []) {
|
||
return 0;
|
||
}
|
||
|
||
return UserAbo::query()
|
||
->activeAbo()
|
||
->where('is_for', $isFor)
|
||
->whereIn($userColumn, $memberIds)
|
||
->count();
|
||
}
|
||
|
||
/**
|
||
* B10: Prognose je Ausführungstermin des LAUFENDEN Monats. Basis ist der
|
||
* heutige Stand (Kundenumsatz + Payline-Punkte, vom Aufrufer übergeben);
|
||
* dazu kommen kumuliert die Abo-Punkte aller noch offenen Ausführungen bis
|
||
* zum jeweiligen Termin (next_date > heute, im laufenden Monat). Bereits
|
||
* ausgeführte Abos stecken im Ist-Umsatz und werden nicht doppelt gezählt.
|
||
*
|
||
* @return array{base: float, days: array<int, array{day: int, date: string, upcoming: float, total: float, is_past: bool}>}
|
||
*/
|
||
public function forecastDays(User $user, float $basePoints): array
|
||
{
|
||
$today = Carbon::today();
|
||
$context = $this->paylineContext($user, (int) $today->month, (int) $today->year);
|
||
|
||
$upcomingAbos = $this->upcomingAboQuery($user, $context['member_ids'], $today)
|
||
->with('user_abo_items.product')
|
||
->get();
|
||
|
||
$pointsByDay = [];
|
||
foreach ($upcomingAbos as $abo) {
|
||
$day = (int) Carbon::parse($abo->getRawOriginal('next_date'))->day;
|
||
$pointsByDay[$day] = ($pointsByDay[$day] ?? 0.0) + (float) $abo->getTotalPoints();
|
||
}
|
||
|
||
$days = [];
|
||
foreach (UserAbo::$aboDeliveryDays as $deliveryDay) {
|
||
$upcoming = 0.0;
|
||
foreach ($pointsByDay as $day => $points) {
|
||
if ($day <= $deliveryDay) {
|
||
$upcoming += $points;
|
||
}
|
||
}
|
||
|
||
$days[] = [
|
||
'day' => $deliveryDay,
|
||
'date' => $today->copy()->startOfMonth()->addDays($deliveryDay - 1)->format(\Util::formatDateDB()),
|
||
'upcoming' => $upcoming,
|
||
'total' => $basePoints + $upcoming,
|
||
'is_past' => $deliveryDay < (int) $today->day,
|
||
];
|
||
}
|
||
|
||
return [
|
||
'base' => $basePoints,
|
||
'days' => $days,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* N4: Level- und Provisions-Prognose zum letzten Liefertag des laufenden
|
||
* Monats (Kundenentscheid F5: gekennzeichnete Schätzung).
|
||
*
|
||
* - Prognose-Basis: heutige Punkte je Payline-Ebene (TP-Laufstände) plus die
|
||
* noch offenen Abo-Ausführungen, ebenengenau zugeordnet; eigene Abos und
|
||
* eigene Kundenabos zählen auf das persönliche Volumen.
|
||
* - Level-Prognose: erreicht die prognostizierte Payline-Punktzahl (inkl.
|
||
* KP-Überschuss, Logik wie {@see BusinessUserItemOptimized::setNextUserLevel()})
|
||
* das nächste Level — und falls nicht, wie viel fehlt laut Prognose?
|
||
* - Provisions-Schätzung: Ebenen-Punkte × Provisionssätze des laut Prognose
|
||
* erreichten Levels; Shop-/Wachstumsanteile werden mit dem heutigen
|
||
* Live-Wert fortgeschrieben. Rangverschiebungen im Team bleiben außen
|
||
* vor — deshalb im UI als unverbindlich kennzeichnen.
|
||
*
|
||
* @return array{next_name: string|null, reached: bool, missing_pp: float, missing_personal: float, personal_required: float, personal_forecast: float, estimated_commission: float, estimated_level_name: string, is_live: bool}|null
|
||
*/
|
||
public function levelAndCommissionForecast(User $user, int $month, int $year): ?array
|
||
{
|
||
$cacheKey = "level_forecast_{$user->id}_{$month}_{$year}";
|
||
$cached = Cache::get($cacheKey);
|
||
if (is_array($cached)) {
|
||
return $cached;
|
||
}
|
||
|
||
$forecast = $this->buildLevelAndCommissionForecast($user, $month, $year);
|
||
|
||
if ($forecast !== null) {
|
||
Cache::put($cacheKey, $forecast, now()->addHours(self::CACHE_HOURS));
|
||
}
|
||
|
||
return $forecast;
|
||
}
|
||
|
||
/**
|
||
* @return array{next_name: string|null, reached: bool, missing_pp: float, missing_personal: float, personal_required: float, personal_forecast: float, estimated_commission: float, estimated_level_name: string, is_live: bool}|null
|
||
*/
|
||
private function buildLevelAndCommissionForecast(User $user, int $month, int $year): ?array
|
||
{
|
||
$snapshot = $this->commissionService->snapshot($user, $month, $year);
|
||
$qualLevel = $snapshot['qual_level'];
|
||
|
||
if (! is_array($qualLevel) || ! empty($snapshot['failed'])) {
|
||
return null;
|
||
}
|
||
|
||
$today = Carbon::today();
|
||
$lineByUserId = $this->lineByUserId($user);
|
||
$upcoming = $this->upcomingPointsByLine($user, $lineByUserId, $today, $month, $year);
|
||
|
||
$linePointsForecast = $this->currentLinePoints($lineByUserId, $month, $year);
|
||
foreach ($upcoming['by_line'] as $line => $points) {
|
||
$linePointsForecast[$line] = ($linePointsForecast[$line] ?? 0.0) + $points;
|
||
}
|
||
|
||
$personalForecast = (float) $snapshot['personal_points'] + $upcoming['personal'];
|
||
$nextLevel = is_array($snapshot['next_level']) ? $snapshot['next_level'] : null;
|
||
|
||
$reached = false;
|
||
$missingPp = 0.0;
|
||
$missingPersonal = 0.0;
|
||
|
||
if ($nextLevel !== null) {
|
||
$restKp = max(0.0, $personalForecast - (float) ($nextLevel['qual_kp'] ?? 0));
|
||
$paylineForecast = $this->sumLinePoints($linePointsForecast, (int) ($nextLevel['paylines'] ?? 0)) + $restKp;
|
||
$missingPp = max(0.0, (float) ($nextLevel['qual_pp'] ?? 0) - $paylineForecast);
|
||
$missingPersonal = max(0.0, (float) ($nextLevel['qual_kp'] ?? 0) - $personalForecast);
|
||
$reached = $missingPp <= 0.0 && $missingPersonal <= 0.0;
|
||
}
|
||
|
||
$estimationLevel = ($reached && $nextLevel !== null) ? $nextLevel : $qualLevel;
|
||
$commissionPp = 0.0;
|
||
for ($line = 1; $line <= (int) ($estimationLevel['paylines'] ?? 0); $line++) {
|
||
$rate = (float) ($estimationLevel['pr_line_'.$line] ?? 0);
|
||
$commissionPp += ($linePointsForecast[$line] ?? 0.0) * $rate / 100;
|
||
}
|
||
|
||
return [
|
||
'next_name' => $nextLevel !== null ? (string) ($nextLevel['name'] ?? '') : null,
|
||
'reached' => $reached,
|
||
'missing_pp' => round($missingPp, 2),
|
||
'missing_personal' => round($missingPersonal, 2),
|
||
'personal_required' => (float) ($nextLevel['qual_kp'] ?? 0),
|
||
'personal_forecast' => round($personalForecast, 2),
|
||
'estimated_commission' => round($commissionPp + (float) $snapshot['shop'] + (float) $snapshot['growth'], 2),
|
||
'estimated_level_name' => (string) ($estimationLevel['name'] ?? ''),
|
||
'is_live' => (bool) $snapshot['is_live'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Ebenen-Zuordnung der gesamten Downline (Linie je User-ID).
|
||
*
|
||
* @return array<int, int>
|
||
*/
|
||
private function lineByUserId(User $user): array
|
||
{
|
||
$map = [];
|
||
|
||
foreach ($this->dashboardService->lineBuckets($user->id) as $line => $users) {
|
||
foreach ($users as $member) {
|
||
$map[(int) $member->id] = (int) $line;
|
||
}
|
||
}
|
||
|
||
return $map;
|
||
}
|
||
|
||
/**
|
||
* Offene Abo-Ausführungen des Monats, aufgeteilt in persönliches Volumen
|
||
* (eigene Abos + eigene Kundenabos) und Team-Punkte je Ebene.
|
||
*
|
||
* @param array<int, int> $lineByUserId
|
||
* @return array{personal: float, by_line: array<int, float>}
|
||
*/
|
||
private function upcomingPointsByLine(User $user, array $lineByUserId, Carbon $today, int $month, int $year): array
|
||
{
|
||
if ((int) $today->month !== $month || (int) $today->year !== $year) {
|
||
return ['personal' => 0.0, 'by_line' => []];
|
||
}
|
||
|
||
$memberIds = array_keys($lineByUserId);
|
||
$personal = 0.0;
|
||
$byLine = [];
|
||
|
||
$abos = $this->upcomingAboQuery($user, $memberIds, $today)
|
||
->with('user_abo_items.product')
|
||
->get();
|
||
|
||
foreach ($abos as $abo) {
|
||
$ownerId = $abo->is_for === 'me' ? (int) $abo->user_id : (int) $abo->member_id;
|
||
$points = (float) $abo->getTotalPoints();
|
||
|
||
if ($ownerId === (int) $user->id) {
|
||
$personal += $points;
|
||
} elseif (isset($lineByUserId[$ownerId])) {
|
||
$byLine[$lineByUserId[$ownerId]] = ($byLine[$lineByUserId[$ownerId]] ?? 0.0) + $points;
|
||
}
|
||
}
|
||
|
||
return ['personal' => $personal, 'by_line' => $byLine];
|
||
}
|
||
|
||
/**
|
||
* Heutige Team-Punkte je Ebene: TP + Shop-Laufstand (letzte
|
||
* user_sales_volumes-Zeile je User) — dieselbe Punktbasis wie die
|
||
* business_lines des TreeCalcBot ({@see UserSalesVolume::getPointsTPSum()}).
|
||
*
|
||
* @param array<int, int> $lineByUserId
|
||
* @return array<int, float>
|
||
*/
|
||
private function currentLinePoints(array $lineByUserId, int $month, int $year): array
|
||
{
|
||
if ($lineByUserId === []) {
|
||
return [];
|
||
}
|
||
|
||
$rows = UserSalesVolume::query()
|
||
->whereIn('user_id', array_keys($lineByUserId))
|
||
->where('month', $month)
|
||
->where('year', $year)
|
||
->whereIn('id', function ($sub) use ($month, $year) {
|
||
$sub->selectRaw('MAX(id)')
|
||
->from('user_sales_volumes')
|
||
->where('month', $month)
|
||
->where('year', $year)
|
||
->groupBy('user_id');
|
||
})
|
||
->get(['user_id', 'month_TP_points', 'month_shop_points']);
|
||
|
||
$byLine = [];
|
||
foreach ($rows as $row) {
|
||
$line = $lineByUserId[(int) $row->user_id] ?? null;
|
||
if ($line !== null) {
|
||
$byLine[$line] = ($byLine[$line] ?? 0.0) + (float) $row->month_TP_points + (float) $row->month_shop_points;
|
||
}
|
||
}
|
||
|
||
return $byLine;
|
||
}
|
||
|
||
/**
|
||
* @param array<int, float> $linePoints
|
||
*/
|
||
private function sumLinePoints(array $linePoints, int $maxLine): float
|
||
{
|
||
$sum = 0.0;
|
||
foreach ($linePoints as $line => $points) {
|
||
if ($line <= $maxLine) {
|
||
$sum += $points;
|
||
}
|
||
}
|
||
|
||
return $sum;
|
||
}
|
||
|
||
/**
|
||
* @return array{0: array{paylines: int, member_ids: int[]}, 1: bool} [Kontext, Lauf fehlgeschlagen]
|
||
*/
|
||
private function buildPaylineContext(User $user, int $month, int $year): array
|
||
{
|
||
$snapshot = $this->commissionService->snapshot($user, $month, $year);
|
||
$paylines = $snapshot['paylines'];
|
||
|
||
if ($paylines <= 0) {
|
||
return [['paylines' => 0, 'member_ids' => []], (bool) ($snapshot['failed'] ?? false)];
|
||
}
|
||
|
||
$memberIds = collect($this->dashboardService->lineBuckets($user->id))
|
||
->filter(fn ($users, int $line) => $line <= $paylines)
|
||
->flatMap(fn ($users) => $users->pluck('id'))
|
||
->map(fn ($id) => (int) $id)
|
||
->values()
|
||
->all();
|
||
|
||
return [['paylines' => $paylines, 'member_ids' => $memberIds], false];
|
||
}
|
||
|
||
/**
|
||
* Soll: Abo-Punkte-Bestand (Produktpunkte × Menge, Hauptartikel comp = 0)
|
||
* aller aktiven Abos der Payline-Mitglieder — Berater-Abos und Kundenabos.
|
||
*
|
||
* @param int[] $memberIds
|
||
*/
|
||
private function aboTargetPoints(array $memberIds): float
|
||
{
|
||
if ($memberIds === []) {
|
||
return 0.0;
|
||
}
|
||
|
||
return (float) UserAbo::query()
|
||
->join('user_abo_items', 'user_abo_items.user_abo_id', '=', 'user_abos.id')
|
||
->join('products', 'products.id', '=', 'user_abo_items.product_id')
|
||
->whereNull('products.deleted_at')
|
||
->where('user_abos.active', true)
|
||
->whereNotIn('user_abos.status', [4, 5, 6])
|
||
->where('user_abo_items.comp', 0)
|
||
->where(function (Builder $query) use ($memberIds) {
|
||
$query->where(fn (Builder $inner) => $inner->where('user_abos.is_for', 'me')->whereIn('user_abos.user_id', $memberIds))
|
||
->orWhere(fn (Builder $inner) => $inner->where('user_abos.is_for', 'ot')->whereIn('user_abos.member_id', $memberIds));
|
||
})
|
||
->selectRaw('COALESCE(SUM(products.points * user_abo_items.qty), 0) as points')
|
||
->value('points');
|
||
}
|
||
|
||
/**
|
||
* Ist: im Monat abgerechnete Abo-Punkte der Payline-Mitglieder.
|
||
*
|
||
* @param int[] $memberIds
|
||
*/
|
||
private function aboActualPoints(array $memberIds, int $month, int $year): float
|
||
{
|
||
if ($memberIds === []) {
|
||
return 0.0;
|
||
}
|
||
|
||
return (float) UserSalesVolume::query()
|
||
->leftJoin('shopping_orders', 'shopping_orders.id', '=', 'user_sales_volumes.shopping_order_id')
|
||
->whereIn('user_id', $memberIds)
|
||
->where('month', $month)
|
||
->where('year', $year)
|
||
->selectRaw(SalesVolumePoints::aboPoints().' as points')
|
||
->value('points');
|
||
}
|
||
|
||
/**
|
||
* Noch offene Abo-Ausführungen des laufenden Monats: aktive Abos der
|
||
* Payline-Mitglieder plus — je nach {@see INCLUDE_OWN_ABOS} — die eigenen
|
||
* Abos/Kundenabos des Betrachters.
|
||
*
|
||
* @param int[] $memberIds
|
||
*/
|
||
private function upcomingAboQuery(User $user, array $memberIds, Carbon $today): Builder
|
||
{
|
||
$aboUserIds = self::INCLUDE_OWN_ABOS
|
||
? array_values(array_unique(array_merge([(int) $user->id], $memberIds)))
|
||
: $memberIds;
|
||
|
||
return UserAbo::query()
|
||
->where('active', true)
|
||
->where('status', 2)
|
||
->where('next_date', '>', $today->copy()->endOfDay())
|
||
->where('next_date', '<=', $today->copy()->endOfMonth())
|
||
->where(function (Builder $query) use ($aboUserIds) {
|
||
$query->where(fn (Builder $inner) => $inner->where('is_for', 'me')->whereIn('user_id', $aboUserIds))
|
||
->orWhere(fn (Builder $inner) => $inner->where('is_for', 'ot')->whereIn('member_id', $aboUserIds));
|
||
});
|
||
}
|
||
}
|