- Bestell-Editor rechts: Zahlungsvorgang (Status, Grund, Transaktions-ID, Referenz, Betraege, Zeiten, Verlauf aus PG payment_log), anbieterneutral ueber payment_transactions (Payments\Overview, Log::forOrder) - markCancelled() fuellt Stornodatum, Stornostatus, Stornogrund und Storno-Nachricht der Bestellung (nur leere Felder) - neuer Stornogrund 'Zahlung beim Anbieter nicht abgeschlossen' (install.sql) - reconcile: 404-Grund lesbar mit Alter des Vorgangs
122 lines
4.6 KiB
PHP
122 lines
4.6 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Payments;
|
|
|
|
/**
|
|
* Lesesicht auf einen Zahlungsvorgang fuers Backend (Bestell-Editor, Block "Zahlungsvorgang").
|
|
*
|
|
* Anbieterneutral: liest payment_transactions (MySQL) und den Verlauf aus payment_log (PG).
|
|
* Kein Provider-Aufruf, keine API -- nur Anzeige. Faellt still auf null zurueck, wenn die
|
|
* Bestellung keinen Vorgang hat (alte Zahlarten, Vorkasse, Rechnung ...).
|
|
*/
|
|
final class Overview
|
|
{
|
|
private const STATUS = [
|
|
'open' => 'offen -- Kunde ist beim Anbieter',
|
|
'pending' => 'in Pruefung beim Anbieter',
|
|
'paid' => 'bezahlt',
|
|
'cancelled' => 'abgebrochen / storniert',
|
|
'mismatch' => 'BETRAG WEICHT AB -- pruefen!',
|
|
];
|
|
|
|
private const SOURCE = [
|
|
'return' => 'Rueckkehr des Kunden vom Anbieter',
|
|
'express' => 'Express-Kauf aus dem Warenkorb',
|
|
'webhook' => 'Meldung des Anbieters (Webhook)',
|
|
'reconcile' => 'Abgleicher (Cron)',
|
|
];
|
|
|
|
private const ACTION = [
|
|
'create' => 'Vorgang beim Anbieter angelegt',
|
|
'start' => 'Kunde zum Anbieter weitergeleitet',
|
|
'capture' => 'Einzug (Capture)',
|
|
'webhook' => 'Meldung vom Anbieter',
|
|
'mail' => 'Bestellbestaetigung an Kunden',
|
|
'cancel' => 'Storno',
|
|
'mismatch' => 'Betragsabweichung',
|
|
'express_create' => 'Express-Kauf gestartet',
|
|
'express_get' => 'Express: Freigabe abgeholt',
|
|
'express_patch' => 'Express: Betrag angepasst',
|
|
'express_reapprove' => 'Express: zweite Freigabe noetig',
|
|
];
|
|
|
|
/**
|
|
* @return array<string,mixed>|null Felder fuers Template oder null ohne Vorgang
|
|
*/
|
|
public static function forOrder(\mysqli $db, int $orderId, ?object $base = null): ?array
|
|
{
|
|
if ($orderId <= 0) {
|
|
return null;
|
|
}
|
|
$stmt = $db->prepare('SELECT * FROM payment_transactions WHERE order_id=? ORDER BY id DESC LIMIT 1');
|
|
if (!$stmt) {
|
|
return null;
|
|
}
|
|
$stmt->bind_param('i', $orderId);
|
|
$stmt->execute();
|
|
$tx = $stmt->get_result()->fetch_assoc();
|
|
$stmt->close();
|
|
if (!$tx) {
|
|
return null;
|
|
}
|
|
|
|
$module = (string) $tx['provider'];
|
|
$label = $module;
|
|
if ($base !== null && Registry::has($module)) {
|
|
$p = Registry::get($module, $base);
|
|
if ($p) {
|
|
$label = $p->label();
|
|
}
|
|
}
|
|
|
|
$status = (string) $tx['status'];
|
|
$detail = (string) ($tx['detail'] ?? '');
|
|
if ($status === 'paid') {
|
|
// markPaid() legt in detail nur die Quelle ab (return/express/webhook/reconcile).
|
|
$detail = 'bezahlt, bestaetigt ueber: ' . (self::SOURCE[$detail] ?? $detail);
|
|
}
|
|
return [
|
|
'provider' => $module,
|
|
'provider_label' => $label,
|
|
'status' => $status,
|
|
'status_label' => self::STATUS[$status] ?? $status,
|
|
'reference' => (string) $tx['reference'],
|
|
'capture_id' => (string) ($tx['capture_id'] ?? ''),
|
|
'expected_amount' => \number_format((float) $tx['expected_amount'], 2, ',', '.') . ' ' . $tx['currency'],
|
|
'paid_amount' => $tx['paid_amount'] !== null ? \number_format((float) $tx['paid_amount'], 2, ',', '.') . ' ' . $tx['currency'] : '',
|
|
'created_at' => self::de((string) $tx['created_at']),
|
|
'paid_at' => self::de((string) ($tx['paid_at'] ?? '')),
|
|
'updated_at' => self::de((string) $tx['updated_at']),
|
|
'detail' => $detail,
|
|
'history' => self::history($orderId),
|
|
];
|
|
}
|
|
|
|
/** @return list<array{zeit:string,aktion:string,status:string,http:string,detail:string}> */
|
|
private static function history(int $orderId): array
|
|
{
|
|
$out = [];
|
|
foreach (Log::forOrder($orderId, 40) as $row) {
|
|
$a = (string) $row['action'];
|
|
$out[] = [
|
|
'zeit' => (string) $row['zeit'],
|
|
'aktion' => self::ACTION[$a] ?? $a,
|
|
'status' => (string) ($row['status'] ?? ''),
|
|
'http' => $row['http_status'] !== null && $row['http_status'] !== '' ? 'HTTP ' . $row['http_status'] : '',
|
|
'detail' => (string) ($row['detail'] ?? ''),
|
|
];
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
private static function de(string $dt): string
|
|
{
|
|
if ($dt === '' || \str_starts_with($dt, '0000')) {
|
|
return '';
|
|
}
|
|
$t = \strtotime($dt);
|
|
return $t ? \date('d.m.Y H:i:s', $t) : $dt;
|
|
}
|
|
}
|