| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /var/www/seaside.pacim.de/web/ |
Upload File : |
<?php
/**
* booking-storno.php – Storno / Storno-Rückgängig einer Buchung (Admin) inkl.
* optionalem Mailversand mit Vorschau-/Bearbeiten-Schritt.
*
* Aktionen (action):
* cancel POST invoice_id -> invoice_status = 3
* uncancel POST invoice_id -> invoice_status = 0
* preview GET/POST invoice_id, kind(storno|confirm) -> {to, subject, html, attachments[]}
* send POST invoice_id, kind, to, subject, html -> versendet (mailservice)
*
* Die eigentliche Storno-Aktion (cancel/uncancel) wird sofort ausgeführt; der Mailversand
* ist ein getrennter, optionaler Schritt mit Vorschau (Inhalt editierbar).
*/
require_once( __DIR__ . '/includes/config.inc.php' );
@ini_set( 'display_errors', '0' );
error_reporting( 0 );
ob_start();
function bs_json( $a ) { while ( ob_get_level() > 0 ) ob_end_clean(); header( 'Content-Type: application/json; charset=utf-8' ); echo json_encode( $a ); exit(); }
function bs_fail( $m ) { bs_json( array( 'ok' => false, 'error' => $m ) ); }
if ( ! function_exists( 'is_admin' ) || ! is_admin() ) { http_response_code( 403 ); bs_json( array( 'ok' => false, 'error' => 'forbidden' ) ); }
$action = $_GET['action'] ?? ( $_POST['action'] ?? '' );
$invoice = (int) ( $_GET['invoice_id'] ?? ( $_POST['invoice_id'] ?? 0 ) );
$db = new MysqliDb( MYSQL_HOST, MYSQL_USER, MYSQL_PASS, MYSQL_DB );
if ( $invoice <= 0 ) bs_fail( 'Ungültige Rechnung.' );
/* ---- Status setzen ---- */
if ( $action === 'cancel' || $action === 'uncancel' ) {
if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) bs_fail( 'method' );
$first = $db->rawQueryOne( "SELECT booking_id, booking_no FROM pacim_booking WHERE booking_invoice = ? ORDER BY booking_id ASC LIMIT 1", array( $invoice ) );
$bid = $first ? (int) $first['booking_id'] : 0;
$logBefore = $bid > 0 ? apilog::snapshot( $db, $bid ) : null;
$db->where( 'invoice_id', $invoice );
$db->update( 'pacim_invoice', array( 'invoice_status' => ( $action === 'cancel' ? 3 : 0 ) ) );
if ( $bid > 0 ) {
apilog::record( array(
'key_name' => trim( (string) ( $_SESSION['user_name'] ?? 'Administrator' ) ),
'ip' => $_SERVER['REMOTE_ADDR'] ?? '', 'method' => 'POST',
'resource' => 'booking', 'action' => ( $action === 'cancel' ? 'storno' : 'unstorno' ), 'kind' => 'write',
'booking' => $bid, 'invoice' => $invoice,
'target' => ( $action === 'cancel' ? 'Buchung storniert' : 'Storno zurückgenommen' ) . ( $first ? ' (' . $first['booking_no'] . ')' : '' ),
'changes' => apilog::diff( $logBefore, apilog::snapshot( $db, $bid ) ),
) );
}
bs_json( array( 'ok' => true ) );
}
/* ---- Mailvorlage des passenden Typs laden ---- */
function bs_template( $db, $kind ) {
$key = ( $kind === 'confirm' ) ? 'booking_confirmation' : 'cancellation';
$t = $db->rawQueryOne( "SELECT * FROM pacim_templates WHERE template_key = ?", array( $key ) );
return $t ? (object) $t : null;
}
/* ---- Vorschau: gerenderte Mail zurückgeben ---- */
if ( $action === 'preview' ) {
$kind = ( ( $_GET['kind'] ?? ( $_POST['kind'] ?? 'storno' ) ) === 'confirm' ) ? 'confirm' : 'storno';
$tpl = bs_template( $db, $kind );
if ( ! $tpl ) bs_fail( 'Vorlage nicht gefunden.' );
$ctx = mailrenderer::context( $invoice );
$r = mailrenderer::render( $tpl, $ctx, false );
$att = mailrenderer::resolveAttachments( (int) $tpl->template_id, $ctx );
$names = array();
foreach ( $att['files'] as $f ) $names[] = $f['name'];
bs_json( array(
'ok' => true,
'to' => trim( (string) ( $ctx['customer_email'] ?? '' ) ),
'subject' => $r['subject'],
'html' => $r['html'],
'attachments' => $names,
) );
}
/* ---- Senden (bearbeiteter Inhalt) ----
* Storno-/Bestätigungsmail läuft bewusst AN DEN REGELN VORBEI (manuelle Aktion, kein
* Regel-Trigger). Trotzdem wird sie – wie die übrige Mail-Pipeline – als Snapshot in
* pacim_messages protokolliert (erscheint im E-Mail-Tab der Buchung) und erhält eine
* Kopie (BCC) an die Firmenadresse. Dedupe: pro Buchung wird je Vorlage nur EINMAL
* versendet (Storno-Mail darf nur einmal pro Buchung rausgehen). */
if ( $action === 'send' ) {
if ( $_SERVER['REQUEST_METHOD'] !== 'POST' ) bs_fail( 'method' );
$kind = ( ( $_POST['kind'] ?? 'storno' ) === 'confirm' ) ? 'confirm' : 'storno';
$to = trim( (string) ( $_POST['to'] ?? '' ) );
$subject = (string) ( $_POST['subject'] ?? '' );
$html = (string) ( $_POST['html'] ?? '' );
if ( ! filter_var( $to, FILTER_VALIDATE_EMAIL ) ) bs_fail( 'Bitte eine gültige Empfängeradresse angeben.' );
if ( trim( $html ) === '' ) bs_fail( 'Kein Inhalt zum Versenden.' );
$tpl = bs_template( $db, $kind );
$tplId = $tpl ? (int) $tpl->template_id : 0;
$files = array();
if ( $tpl ) {
$ctx = mailrenderer::context( $invoice );
$att = mailrenderer::resolveAttachments( $tplId, $ctx );
$files = $att['files'];
}
// Buchung/Kunde für Protokoll + Dedupe ermitteln
$cust = $db->rawQueryOne( "SELECT invoice_customer FROM pacim_invoice WHERE invoice_id=?", array( $invoice ) );
$bk = $db->rawQueryOne( "SELECT booking_id FROM pacim_booking WHERE booking_invoice=? ORDER BY booking_id ASC LIMIT 1", array( $invoice ) );
$bookingId = (int) ( $bk['booking_id'] ?? 0 );
// Dedupe: je Vorlage + Buchung nur ein Versand (bereits gesendet ODER in Queue wartend)
if ( $tplId > 0 && $bookingId > 0 ) {
$dup = $db->rawQueryOne(
"SELECT message_id FROM pacim_messages WHERE message_template=? AND message_booking=? AND message_status IN (1,5) LIMIT 1",
array( $tplId, $bookingId )
);
if ( $dup ) bs_fail( $kind === 'confirm'
? 'Für diese Buchung wurde bereits eine Bestätigung versendet.'
: 'Für diese Buchung wurde bereits eine Storno-Mail versendet.' );
}
// Konto & BCC (Kopie): vorhandene Regel zur Vorlage nutzen, sonst Firmenabsender als Kopie.
$opts = array(); $ruleAccount = 0; $ruleBcc = null;
if ( $tplId > 0 ) {
$rule = $db->rawQueryOne( "SELECT rule_account, rule_bcc FROM pacim_mail_rules WHERE rule_template=? AND rule_active=1 ORDER BY rule_id ASC LIMIT 1", array( $tplId ) );
if ( $rule ) {
if ( (int) $rule['rule_account'] > 0 ) { $ruleAccount = (int) $rule['rule_account']; $opts['account'] = $ruleAccount; }
if ( trim( (string) $rule['rule_bcc'] ) !== '' ) $ruleBcc = trim( (string) $rule['rule_bcc'] );
}
}
$cfg = mailservice::config();
if ( $ruleBcc === null || $ruleBcc === '' ) {
$fallback = trim( (string) $cfg['from_email'] ); // Firmenadresse als Kopie, wenn keine Regel-BCC
if ( $fallback !== '' && filter_var( $fallback, FILTER_VALIDATE_EMAIL ) ) $ruleBcc = $fallback;
}
if ( $ruleBcc !== null && $ruleBcc !== '' ) $opts['bcc'] = $ruleBcc;
// Snapshot anlegen (erscheint in der E-Mail-Liste der Buchung). message_rule bleibt NULL
// -> der Queue-Versand prüft KEINE Regel (Storno-Mail soll trotz stornierter Rechnung raus).
$live = mailservice::smtpEnabled();
$msgId = $db->insert( 'pacim_messages', array(
'message_status' => 5, 'message_type' => 'system', 'message_count' => 0,
'message_template' => $tplId, 'message_customer' => (int) ( $cust['invoice_customer'] ?? 0 ),
'message_invoice' => $invoice, 'message_booking' => $bookingId,
'message_subject' => $subject, 'message_sender' => $cfg['from_email'], 'message_receiver' => $to,
'message_account' => $ruleAccount, 'message_bcc' => $ruleBcc,
'message_content' => $html, 'message_token' => pacim_sign( 'msg:' . uniqid( '', true ) ),
'message_datetime' => date( 'Y-m-d H:i:s' ),
) );
if ( ! $live ) {
bs_json( array( 'ok' => true, 'queued' => true, 'message' => 'E-Mail in die Warteschlange gelegt – sie wird versendet, sobald der SMTP-Versand aktiviert ist.' ) );
}
$res = mailservice::send( $to, '', $subject, $html, $files, $opts );
if ( ! empty( $res['ok'] ) ) {
$db->where( 'message_id', (int) $msgId );
$db->update( 'pacim_messages', array( 'message_status' => 1, 'message_count' => 1, 'message_error' => null ) );
bs_json( array( 'ok' => true, 'message' => 'E-Mail versendet.' ) );
}
// Fehler: Snapshot bleibt in der Queue (Cron versucht erneut)
$db->where( 'message_id', (int) $msgId );
$db->update( 'pacim_messages', array( 'message_count' => 1, 'message_error' => mb_substr( (string) ( $res['error'] ?? 'unbekannt' ), 0, 500 ) ) );
bs_fail( 'Versand fehlgeschlagen: ' . ( $res['error'] ?? 'unbekannt' ) . '. Die E-Mail bleibt in der Warteschlange und wird automatisch erneut versucht.' );
}
bs_fail( 'unknown_action' );