= (Y-m-d)} {--until= : Nur Bestellungen mit created_at <= Ende dieses Tages (Y-m-d)} {--order= : Komma-getrennte shopping_order IDs (Filter)} {--mode=live : Modus: live, test, dev oder all} {--stats : Zusaetzliche Statistik: bezahlte Abo-Bestellungen vs. mit/ohne UserAboOrder}'; protected $description = 'Abgleich und Reparatur: bezahlte Abo-Bestellungen (Checkout) ohne Verknuepfung user_abo_orders — z. B. nach Payone-Callback vor Erfolgs-Redirect'; public function handle(): int { $missing = $this->queryMissingOrders()->orderBy('id')->get(); $this->info('Abgleich: Bestellungen mit is_abo, abo_interval>0, als bezahlt markiert, ohne user_abo_orders-Eintrag.'); $this->newLine(); if ($this->option('stats')) { $this->printStats(); $this->newLine(); } $this->info('Treffer (fehlende Verknuepfung): '.$missing->count()); if ($missing->isEmpty()) { $this->info('Keine Diskrepanz — nichts zu tun.'); return self::SUCCESS; } $this->table( ['ID', 'shopping_user_id', 'mode', 'txaction', 'paid', 'created_at'], $missing->take(200)->map(fn (ShoppingOrder $o) => [ $o->id, $o->shopping_user_id, $o->mode, $o->txaction, $o->paid ? '1' : '0', $o->created_at?->format('Y-m-d H:i'), ]) ); if ($missing->count() > 200) { $this->warn('… und weitere '.($missing->count() - 200).' Eintraege (Ausgabe gekuerzt).'); } if (! $this->option('fix')) { $this->newLine(); $this->warn('Trockenlauf. Nutze --fix zur Reparatur (mit Bestaetigung).'); return self::SUCCESS; } if (! $this->option('force') && ! $this->confirm('Wirklich '.$missing->count().' Bestellung(en) reparieren?')) { return self::SUCCESS; } $ok = 0; $fail = 0; $bar = $this->output->createProgressBar($missing->count()); $bar->start(); foreach ($missing as $order) { try { DB::transaction(function () use ($order) { $this->repairSingleOrder($order); }); $ok++; } catch (\Throwable $e) { $fail++; $this->newLine(); $this->error("Order #{$order->id}: {$e->getMessage()}"); } $bar->advance(); } $bar->finish(); $this->newLine(2); $this->info("Fertig: {$ok} repariert, {$fail} Fehler."); return $fail > 0 ? self::FAILURE : self::SUCCESS; } /** * @return \Illuminate\Database\Eloquent\Builder */ private function queryMissingOrders(): \Illuminate\Database\Eloquent\Builder { $q = ShoppingOrder::query() ->where('is_abo', true) ->where('abo_interval', '>', 0) ->where(function ($sub) { $sub->where('paid', true) ->orWhere('paid', 1); }) ->whereIn('txaction', ['paid', 'invoice_paid', 'extern_paid']) ->whereNotNull('shopping_user_id') ->whereHas('shopping_payments') ->whereNotExists(function ($sub) { $sub->select(DB::raw('1')) ->from('user_abo_orders') ->whereColumn('user_abo_orders.shopping_order_id', 'shopping_orders.id'); }); if ($ids = $this->parseOrderIds()) { $q->whereIn('id', $ids); } if ($since = $this->option('since')) { $q->where('created_at', '>=', $since.' 00:00:00'); } if ($until = $this->option('until')) { $q->where('created_at', '<=', $until.' 23:59:59'); } $mode = (string) $this->option('mode'); if ($mode !== 'all') { $q->where('mode', $mode); } return $q; } /** * @return list */ private function parseOrderIds(): array { $raw = $this->option('order'); if ($raw === null || $raw === '') { return []; } return array_values(array_filter(array_map('intval', explode(',', (string) $raw)))); } private function printStats(): void { $mode = (string) $this->option('mode'); $base = ShoppingOrder::query() ->where('is_abo', true) ->where('abo_interval', '>', 0) ->where(function ($sub) { $sub->where('paid', true)->orWhere('paid', 1); }) ->whereIn('txaction', ['paid', 'invoice_paid', 'extern_paid']); if ($since = $this->option('since')) { $base->where('created_at', '>=', $since.' 00:00:00'); } if ($until = $this->option('until')) { $base->where('created_at', '<=', $until.' 23:59:59'); } if ($mode !== 'all') { $base->where('mode', $mode); } if ($ids = $this->parseOrderIds()) { $base->whereIn('id', $ids); } $totalPaidAbo = (clone $base)->count(); $withLink = (clone $base)->whereExists(function ($sub) { $sub->select(DB::raw('1')) ->from('user_abo_orders') ->whereColumn('user_abo_orders.shopping_order_id', 'shopping_orders.id'); })->count(); $this->table( ['Kennzahl', 'Anzahl'], [ ['Bezahlte Abo-Bestellungen (Filter)', $totalPaidAbo], ['Davon mit user_abo_orders', $withLink], ['Davon ohne user_abo_orders', max(0, $totalPaidAbo - $withLink)], ] ); } private function repairSingleOrder(ShoppingOrder $order): void { $payment = ShoppingPayment::query() ->where('shopping_order_id', $order->id) ->orderByDesc('id') ->first(); if (! $payment) { throw new \RuntimeException('Kein ShoppingPayment zur Bestellung.'); } $order->loadMissing(['shopping_user', 'shopping_order_items']); $payment->loadMissing(['payment_transactions']); $payment->setRelation('shopping_order', $order); AboHelper::createNewAbo($payment); $order->refresh(); if (! $order->getUserAbo()) { throw new \RuntimeException('createNewAbo hat kein UserAbo erzeugt (pruefen: abo_interval, Bestellpositionen, ShoppingPayment.abo_interval).'); } AboHelper::setAboActive($order, 2, true); IncentiveTracker::trackAboActivated($order); } }