mivita/app/Console/Commands/PayoneFailedPaypalReport.php
2026-04-10 17:15:27 +02:00

419 lines
18 KiB
PHP

<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class PayoneFailedPaypalReport extends Command
{
protected $signature = 'payone:failed-paypal-report
{--from=2026-04-02 : Start-Datum (YYYY-MM-DD)}
{--to= : End-Datum (YYYY-MM-DD), Standard: heute}
{--output=storage/reports/paypal-failed-report.csv : Ausgabedatei}';
protected $description = 'Erstellt einen Schadenbericht über fehlgeschlagene PayPal-Zahlungen (Error 923)';
public function handle(): int
{
$from = $this->option('from');
$to = $this->option('to') ?: now()->format('Y-m-d');
$outputPath = $this->option('output');
$this->info("Schadenbericht PayPal-Ausfälle: {$from} bis {$to}");
$this->newLine();
$orders = $this->getAffectedOrders($from, $to);
if ($orders->isEmpty()) {
$this->warn('Keine fehlgeschlagenen PayPal-Zahlungen im angegebenen Zeitraum gefunden.');
return self::SUCCESS;
}
$this->displaySummary($orders, $from, $to);
$fullPath = base_path($outputPath);
$dir = dirname($fullPath);
if (! is_dir($dir)) {
mkdir($dir, 0755, true);
}
$this->writeCsvReport($fullPath, $orders, $from, $to);
$this->writeTxtReport(str_replace('.csv', '.txt', $fullPath), $orders, $from, $to);
$this->writeEmailLists($dir, $orders);
$this->newLine();
$this->info("CSV-Bericht: {$fullPath}");
$this->info('TXT-Bericht: ' . str_replace('.csv', '.txt', $fullPath));
$this->info("E-Mail Berater: {$dir}/emails-berater.csv");
$this->info("E-Mail Shop-Kunden: {$dir}/emails-shop-kunden.csv");
return self::SUCCESS;
}
private function getAffectedOrders(string $from, string $to): \Illuminate\Support\Collection
{
return DB::table('shopping_orders')
->join('shopping_payments', function ($join) {
$join->on('shopping_payments.shopping_order_id', '=', 'shopping_orders.id')
->where('shopping_payments.clearingtype', '=', 'wlt')
->where('shopping_payments.wallettype', '=', 'PPE');
})
->join('payment_transactions', function ($join) {
$join->on('payment_transactions.shopping_payment_id', '=', 'shopping_payments.id')
->where('payment_transactions.errorcode', '=', 923);
})
->join('shopping_users', 'shopping_users.id', '=', 'shopping_orders.shopping_user_id')
->whereBetween('payment_transactions.created_at', ["{$from} 00:00:00", "{$to} 23:59:59"])
->select(
'shopping_orders.id as order_id',
'shopping_orders.total_shipping',
'shopping_orders.paid',
'shopping_orders.txaction',
'shopping_orders.mode',
'shopping_orders.payment_for',
'shopping_orders.auth_user_id',
'shopping_orders.created_at as order_date',
'shopping_users.billing_email',
'shopping_users.billing_firstname',
'shopping_users.billing_lastname',
'shopping_payments.id as payment_id',
'shopping_payments.reference',
'shopping_payments.amount as amount_cents',
'shopping_payments.currency',
'payment_transactions.id as tx_id',
'payment_transactions.errorcode',
'payment_transactions.errormessage',
'payment_transactions.created_at as error_date',
)
->orderBy('payment_transactions.created_at')
->get();
}
private function displaySummary(\Illuminate\Support\Collection $rows, string $from, string $to): void
{
$uniqueOrders = $rows->unique('order_id');
$paidOrders = $uniqueOrders->where('paid', 1);
$unpaidOrders = $uniqueOrders->where('paid', 0);
$this->table(
['Kennzahl', 'Wert'],
[
['Zeitraum', "{$from} bis {$to}"],
['Fehlgeschlagene Transaktionen (Error 923)', $rows->count()],
['Betroffene Bestellungen (eindeutig)', $uniqueOrders->count()],
['Davon nachträglich bezahlt (andere Zahlungsart)', $paidOrders->count()],
['Nicht bezahlt (offen/verloren)', $unpaidOrders->count()],
['Summe nicht bezahlter Bestellungen', number_format($unpaidOrders->sum('total_shipping'), 2, ',', '.') . ' EUR'],
['Summe aller betroffenen Bestellungen', number_format($uniqueOrders->sum('total_shipping'), 2, ',', '.') . ' EUR'],
]
);
}
private function writeCsvReport(string $path, \Illuminate\Support\Collection $rows, string $from, string $to): void
{
$fp = fopen($path, 'w');
fprintf($fp, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($fp, [
'Fehler-Datum',
'Bestell-Nr',
'Bestell-Datum',
'Transaktions-ID',
'Payment-Referenz',
'Betrag (EUR)',
'Fehlercode',
'Fehlermeldung',
'Modus',
'Nachträglich bezahlt',
'Aktueller Status',
], ';');
$uniqueOrders = $rows->unique('order_id');
$unpaidOrders = $uniqueOrders->where('paid', 0);
foreach ($rows as $row) {
fputcsv($fp, [
$row->error_date,
$row->order_id,
$row->order_date,
$row->tx_id,
$row->reference,
number_format($row->total_shipping, 2, ',', ''),
$row->errorcode,
$row->errormessage,
$row->mode,
$row->paid ? 'Ja' : 'Nein',
$row->txaction,
], ';');
}
fputcsv($fp, [], ';');
fputcsv($fp, ['ZUSAMMENFASSUNG'], ';');
fputcsv($fp, ['Zeitraum', "{$from} bis {$to}"], ';');
fputcsv($fp, ['Fehlgeschlagene Transaktionen', $rows->count()], ';');
fputcsv($fp, ['Betroffene Bestellungen', $uniqueOrders->count()], ';');
fputcsv($fp, ['Nicht bezahlt (offen/verloren)', $unpaidOrders->count()], ';');
fputcsv($fp, ['Summe nicht bezahlter Bestellungen', number_format($unpaidOrders->sum('total_shipping'), 2, ',', '') . ' EUR'], ';');
fputcsv($fp, ['Summe aller betroffenen Bestellungen', number_format($uniqueOrders->sum('total_shipping'), 2, ',', '') . ' EUR'], ';');
fclose($fp);
}
private function writeTxtReport(string $path, \Illuminate\Support\Collection $rows, string $from, string $to): void
{
$uniqueOrders = $rows->unique('order_id');
$paidOrders = $uniqueOrders->where('paid', 1);
$unpaidOrders = $uniqueOrders->where('paid', 0);
$lines = [];
$lines[] = '================================================================================';
$lines[] = ' SCHADENBERICHT: Fehlgeschlagene PayPal-Zahlungen (PAYONE Error 923)';
$lines[] = '================================================================================';
$lines[] = '';
$lines[] = "Zeitraum: {$from} bis {$to}";
$lines[] = 'Erstellt am: ' . now()->format('d.m.Y H:i:s');
$lines[] = 'Ursache: PayPal-Kontoverknüpfung bei PAYONE nicht migriert (Vertragsübernahme GmbH)';
$lines[] = 'Portal-ID: 2030693';
$lines[] = 'Merchant-ID: 42504';
$lines[] = 'Sub-Account-ID: 43065';
$lines[] = '';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = ' ZUSAMMENFASSUNG';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = '';
$lines[] = sprintf(' Fehlgeschlagene Transaktionen (Error 923): %d', $rows->count());
$lines[] = sprintf(' Betroffene Bestellungen (eindeutig): %d', $uniqueOrders->count());
$lines[] = sprintf(' Davon nachträglich bezahlt (andere Methode): %d', $paidOrders->count());
$lines[] = sprintf(' Nicht bezahlt (offen/verloren): %d', $unpaidOrders->count());
$lines[] = '';
$lines[] = sprintf(' Summe nicht bezahlter Bestellungen: %s EUR', number_format($unpaidOrders->sum('total_shipping'), 2, ',', '.'));
$lines[] = sprintf(' Summe nachträglich bezahlter Bestellungen: %s EUR', number_format($paidOrders->sum('total_shipping'), 2, ',', '.'));
$lines[] = sprintf(' Summe ALLER betroffenen Bestellungen: %s EUR', number_format($uniqueOrders->sum('total_shipping'), 2, ',', '.'));
$lines[] = '';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = ' AUFSCHLÜSSELUNG NACH TAG';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = '';
$byDate = $rows->groupBy(fn($r) => substr($r->error_date, 0, 10));
foreach ($byDate as $date => $dayRows) {
$dayOrders = $dayRows->unique('order_id');
$dayUnpaid = $dayOrders->where('paid', 0);
$lines[] = sprintf(
' %s: %3d Fehler | %3d Bestellungen | %3d nicht bezahlt | %s EUR offen',
$date,
$dayRows->count(),
$dayOrders->count(),
$dayUnpaid->count(),
number_format($dayUnpaid->sum('total_shipping'), 2, ',', '.')
);
}
$lines[] = '';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = ' NICHT BEZAHLTE BESTELLUNGEN (DETAIL)';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = '';
$lines[] = sprintf(
' %-12s %-20s %-18s %-14s %s',
'Bestell-Nr',
'Datum',
'Referenz',
'Betrag (EUR)',
'Status'
);
$lines[] = ' ' . str_repeat('-', 80);
foreach ($unpaidOrders->sortBy('order_date') as $order) {
$lines[] = sprintf(
' %-12s %-20s %-18s %14s %s',
$order->order_id,
$order->order_date,
$order->reference,
number_format($order->total_shipping, 2, ',', '.'),
$order->txaction
);
}
$lines[] = ' ' . str_repeat('-', 80);
$lines[] = sprintf(
' %-12s %-20s %-18s %14s',
'GESAMT',
'',
$unpaidOrders->count() . ' Bestellungen',
number_format($unpaidOrders->sum('total_shipping'), 2, ',', '.')
);
$lines[] = '';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = ' NACHTRÄGLICH BEZAHLTE BESTELLUNGEN (andere Zahlungsart)';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = '';
if ($paidOrders->isEmpty()) {
$lines[] = ' Keine.';
} else {
$lines[] = sprintf(
' %-12s %-20s %-18s %-14s %s',
'Bestell-Nr',
'Datum',
'Referenz',
'Betrag (EUR)',
'Status'
);
$lines[] = ' ' . str_repeat('-', 80);
foreach ($paidOrders->sortBy('order_date') as $order) {
$lines[] = sprintf(
' %-12s %-20s %-18s %14s %s',
$order->order_id,
$order->order_date,
$order->reference,
number_format($order->total_shipping, 2, ',', '.'),
$order->txaction
);
}
$lines[] = ' ' . str_repeat('-', 80);
$lines[] = sprintf(
' %-12s %-20s %-18s %14s',
'GESAMT',
'',
$paidOrders->count() . ' Bestellungen',
number_format($paidOrders->sum('total_shipping'), 2, ',', '.')
);
}
$lines[] = '';
$this->appendEmailSectionToTxt($lines, $unpaidOrders);
$lines[] = '';
$lines[] = '================================================================================';
$lines[] = ' Ende des Berichts';
$lines[] = '================================================================================';
$lines[] = '';
file_put_contents($path, implode("\n", $lines));
}
private function appendEmailSectionToTxt(array &$lines, \Illuminate\Support\Collection $unpaidOrders): void
{
$berater = $unpaidOrders->filter(fn($o) => ! empty($o->auth_user_id))->sortBy('billing_email');
$shopKunden = $unpaidOrders->filter(fn($o) => empty($o->auth_user_id))->sortBy('billing_email');
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = ' BETROFFENE BERATER (mit Auth-User-ID) - nicht bezahlt';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = '';
$lines[] = sprintf(' %-12s %-8s %-30s %-30s %14s', 'Bestell-Nr', 'User-ID', 'Name', 'E-Mail', 'Betrag (EUR)');
$lines[] = ' ' . str_repeat('-', 100);
$beraterSum = 0;
foreach ($berater as $order) {
$name = trim(($order->billing_firstname ?? '') . ' ' . ($order->billing_lastname ?? ''));
$lines[] = sprintf(
' %-12s %-8s %-30s %-30s %14s',
$order->order_id,
$order->auth_user_id,
mb_substr($name, 0, 28),
mb_substr($order->billing_email ?? '-', 0, 28),
number_format($order->total_shipping, 2, ',', '.')
);
$beraterSum += $order->total_shipping;
}
$lines[] = ' ' . str_repeat('-', 100);
$lines[] = sprintf(' %-12s %-8s %-30s %-30s %14s', 'GESAMT', '', $berater->count() . ' Bestellungen', '', number_format($beraterSum, 2, ',', '.'));
$lines[] = '';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = ' BETROFFENE SHOP-KUNDEN (ohne Auth-User-ID) - nicht bezahlt';
$lines[] = '--------------------------------------------------------------------------------';
$lines[] = '';
$lines[] = sprintf(' %-12s %-30s %-30s %14s', 'Bestell-Nr', 'Name', 'E-Mail', 'Betrag (EUR)');
$lines[] = ' ' . str_repeat('-', 90);
$shopSum = 0;
foreach ($shopKunden as $order) {
$name = trim(($order->billing_firstname ?? '') . ' ' . ($order->billing_lastname ?? ''));
$lines[] = sprintf(
' %-12s %-30s %-30s %14s',
$order->order_id,
mb_substr($name, 0, 28),
mb_substr($order->billing_email ?? '-', 0, 28),
number_format($order->total_shipping, 2, ',', '.')
);
$shopSum += $order->total_shipping;
}
$lines[] = ' ' . str_repeat('-', 90);
$lines[] = sprintf(' %-12s %-30s %-30s %14s', 'GESAMT', $shopKunden->count() . ' Bestellungen', '', number_format($shopSum, 2, ',', '.'));
}
private function writeEmailLists(string $dir, \Illuminate\Support\Collection $rows): void
{
$unpaidOrders = $rows->unique('order_id')->where('paid', 0);
$berater = $unpaidOrders->filter(fn($o) => ! empty($o->auth_user_id))->sortBy('order_date');
$shopKunden = $unpaidOrders->filter(fn($o) => empty($o->auth_user_id))->sortBy('order_date');
$this->writeEmailCsv("{$dir}/emails-berater.csv", $berater, true);
$this->writeEmailCsv("{$dir}/emails-shop-kunden.csv", $shopKunden, false);
$this->newLine();
$this->table(
['Kategorie', 'Bestellungen', 'Eindeutige E-Mails', 'Summe (EUR)'],
[
[
'Berater (mit Auth-User-ID)',
$berater->count(),
$berater->pluck('billing_email')->filter()->unique()->count(),
number_format($berater->sum('total_shipping'), 2, ',', '.') . ' EUR',
],
[
'Shop-Kunden (ohne Auth-User-ID)',
$shopKunden->count(),
$shopKunden->pluck('billing_email')->filter()->unique()->count(),
number_format($shopKunden->sum('total_shipping'), 2, ',', '.') . ' EUR',
],
]
);
}
private function writeEmailCsv(string $path, \Illuminate\Support\Collection $orders, bool $includeUserId): void
{
$fp = fopen($path, 'w');
fprintf($fp, chr(0xEF) . chr(0xBB) . chr(0xBF));
$headers = ['Bestell-Nr', 'Bestell-Datum', 'Vorname', 'Nachname', 'E-Mail', 'Betrag (EUR)', 'Status'];
if ($includeUserId) {
array_splice($headers, 1, 0, 'Auth-User-ID');
}
fputcsv($fp, $headers, ';');
foreach ($orders as $order) {
$row = [
$order->order_id,
$order->order_date,
$order->billing_firstname ?? '',
$order->billing_lastname ?? '',
$order->billing_email ?? '',
number_format($order->total_shipping, 2, ',', ''),
$order->txaction,
];
if ($includeUserId) {
array_splice($row, 1, 0, $order->auth_user_id);
}
fputcsv($fp, $row, ';');
}
fputcsv($fp, [], ';');
fputcsv($fp, ['GESAMT', '', '', '', $orders->count() . ' Bestellungen', number_format($orders->sum('total_shipping'), 2, ',', '') . ' EUR'], ';');
fputcsv($fp, ['Eindeutige E-Mail-Adressen', $orders->pluck('billing_email')->filter()->unique()->count()], ';');
fclose($fp);
}
}