220 lines
7.3 KiB
PHP
220 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use App\Models\ShoppingOrder;
|
|
use App\Models\ShoppingPayment;
|
|
use App\Services\AboHelper;
|
|
use App\Services\Incentive\IncentiveTracker;
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
class RepairMissingAboFromOrders extends Command
|
|
{
|
|
protected $signature = 'abo:repair-missing
|
|
{--fix : Reparatur ausfuehren (ohne: nur Abgleich/Vorschau)}
|
|
{--force : Mit --fix: ohne Rueckfrage (Skripte/CI)}
|
|
{--since= : Nur Bestellungen mit created_at >= (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<ShoppingOrder>
|
|
*/
|
|
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<int>
|
|
*/
|
|
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);
|
|
}
|
|
}
|