feat(payments): Block Zahlungsvorgang im Bestell-Editor, Storno-Felder bei automatischem Storno
- 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
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user