- 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
132 lines
4.4 KiB
PHP
132 lines
4.4 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Payments;
|
|
|
|
/**
|
|
* Protokoll aller Anbieter-Antworten in PostgreSQL (krempl_data.payment_log).
|
|
*
|
|
* Warum PG: MySQL `orders` hat keine Uhrzeit und keinen Platz fuer JSON. Hier liegt
|
|
* jede Anfrage/Antwort mit Zeitstempel -- die Beweiskette bei Rueckfragen von
|
|
* Kunden oder Anbietern.
|
|
*
|
|
* Faellt STILL aus (wie shop_event): ein Protokoll darf den Checkout nie gefaehrden.
|
|
* Verbindung kommt aus core/postgres/config.ini, Sektion [postgresql].
|
|
*/
|
|
final class Log
|
|
{
|
|
/** @var resource|\PgSql\Connection|null|false */
|
|
private static $conn = null;
|
|
private static bool $dead = false;
|
|
|
|
/**
|
|
* @param array<mixed>|null $payload wird als JSON abgelegt (Secrets vorher entfernen!)
|
|
*/
|
|
public static function write(
|
|
string $provider,
|
|
string $action,
|
|
?int $orderId = null,
|
|
?string $orderNumber = null,
|
|
?string $reference = null,
|
|
?string $status = null,
|
|
?int $httpStatus = null,
|
|
?array $payload = null,
|
|
?string $detail = null
|
|
): bool {
|
|
$c = self::conn();
|
|
if (!$c) {
|
|
return false;
|
|
}
|
|
$json = $payload === null ? null : \json_encode(self::scrub($payload), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
$ok = @\pg_query_params(
|
|
$c,
|
|
'INSERT INTO payment_log
|
|
(provider, action, order_id, order_number, reference, status, http_status, payload, detail, host)
|
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)',
|
|
[
|
|
$provider, $action, $orderId, $orderNumber, $reference, $status, $httpStatus,
|
|
$json, $detail, (string) ($_SERVER['SERVER_NAME'] ?? \gethostname()),
|
|
]
|
|
);
|
|
if (!$ok) {
|
|
self::$dead = true;
|
|
\error_log('payment_log: INSERT fehlgeschlagen -- ' . @\pg_last_error($c));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Verlauf einer Bestellung fuer den Bestell-Editor, aelteste zuerst.
|
|
* Payload bleibt draussen (Roh-JSON gehoert nicht in die Maske).
|
|
*
|
|
* @return list<array{zeit:string,action:string,status:?string,http_status:?string,detail:?string,reference:?string}>
|
|
*/
|
|
public static function forOrder(int $orderId, int $limit = 40): array
|
|
{
|
|
$c = self::conn();
|
|
if (!$c || $orderId <= 0) {
|
|
return [];
|
|
}
|
|
$host = (string) ($_SERVER['SERVER_NAME'] ?? \gethostname());
|
|
$res = @\pg_query_params(
|
|
$c,
|
|
"SELECT to_char(zeitpunkt, 'DD.MM.YYYY HH24:MI:SS') AS zeit, action, status, http_status, detail, reference
|
|
FROM payment_log
|
|
WHERE order_id = $1 AND host = $2
|
|
ORDER BY id
|
|
LIMIT $3",
|
|
[$orderId, $host, $limit]
|
|
);
|
|
if (!$res) {
|
|
return [];
|
|
}
|
|
return \pg_fetch_all($res) ?: [];
|
|
}
|
|
|
|
/** @return resource|\PgSql\Connection|false */
|
|
private static function conn()
|
|
{
|
|
if (self::$dead) {
|
|
return false;
|
|
}
|
|
if (self::$conn !== null) {
|
|
return self::$conn;
|
|
}
|
|
if (!\function_exists('pg_connect')) {
|
|
self::$dead = true;
|
|
return false;
|
|
}
|
|
$ini = @\parse_ini_file(\dirname(__DIR__) . '/postgres/config.ini', true);
|
|
if (!$ini || empty($ini['postgresql'])) {
|
|
self::$dead = true;
|
|
return false;
|
|
}
|
|
$p = $ini['postgresql'];
|
|
$dsn = \sprintf(
|
|
'host=%s port=%s dbname=%s user=%s password=%s connect_timeout=2',
|
|
$p['host'] ?? 'localhost', $p['port'] ?? '5432', $p['database'] ?? '', $p['user'] ?? '', $p['password'] ?? ''
|
|
);
|
|
$c = @\pg_connect($dsn);
|
|
if (!$c) {
|
|
self::$dead = true;
|
|
\error_log('payment_log: keine PG-Verbindung -- Protokoll uebersprungen');
|
|
return false;
|
|
}
|
|
return self::$conn = $c;
|
|
}
|
|
|
|
/** Entfernt Schluessel, die nach Zugangsdaten aussehen, rekursiv. */
|
|
private static function scrub(array $data): array
|
|
{
|
|
foreach ($data as $k => $v) {
|
|
if (\is_string($k) && \preg_match('/secret|password|token|authorization|client_id/i', $k)) {
|
|
$data[$k] = '***';
|
|
} elseif (\is_array($v)) {
|
|
$data[$k] = self::scrub($v);
|
|
}
|
|
}
|
|
return $data;
|
|
}
|
|
}
|