Basic functionality ready

This commit is contained in:
Mark Pinkster 2025-12-31 19:32:21 +01:00
parent 7a4a45e227
commit 780403843c
3 changed files with 261 additions and 159 deletions

131
api.php
View File

@ -1,7 +1,6 @@
<?php <?php
/** /**
* TELVERO BACKOFFICE - API PROXY (V7.0 FINAL - WITH 100 EURO THRESHOLD) * TELVERO BACKOFFICE - API PROXY (V8.1 - LOGGING RESTORED)
*/ */
session_start(); session_start();
ini_set('display_errors', 0); ini_set('display_errors', 0);
@ -19,62 +18,58 @@ use Mollie\Api\MollieApiClient;
header('Content-Type: application/json'); header('Content-Type: application/json');
// --- DATABASE CONNECTIE ---
$db = new mysqli($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASS'], $_ENV['DB_NAME']); $db = new mysqli($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASS'], $_ENV['DB_NAME']);
if ($db->connect_error) { if ($db->connect_error) { die(json_encode(['error' => 'Database connectie mislukt'])); }
die(json_encode(['error' => 'Database connectie mislukt']));
function writeLog($action, $details) {
global $db;
$user = $_SESSION['user'] ?? 'system';
$stmt = $db->prepare("INSERT INTO sales_logs (username, action, details, created_at) VALUES (?, ?, ?, NOW())");
$stmt->bind_param("sss", $user, $action, $details);
$stmt->execute();
} }
$action = $_GET['action'] ?? ''; $action = $_GET['action'] ?? '';
// --- 1. LOGIN ACTIE --- // --- AUTH ---
if ($action === 'login') { if ($action === 'login') {
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
$stmt = $db->prepare("SELECT password, full_name FROM sales_users WHERE username = ?"); $stmt = $db->prepare("SELECT password, full_name FROM sales_users WHERE username = ?");
$stmt->bind_param("s", $input['username']); $stmt->bind_param("s", $input['username']);
$stmt->execute(); $stmt->execute();
$res = $stmt->get_result()->fetch_assoc(); $res = $stmt->get_result()->fetch_assoc();
if ($res && password_verify($input['password'], $res['password'])) { if ($res && password_verify($input['password'], $res['password'])) {
$_SESSION['user'] = $input['username']; $_SESSION['user'] = $input['username'];
$_SESSION['full_name'] = $res['full_name']; $_SESSION['full_name'] = $res['full_name'];
writeLog('LOGIN', 'Gebruiker ingelogd');
session_write_close(); session_write_close();
echo json_encode(['success' => true, 'user' => $res['full_name']]); echo json_encode(['success' => true, 'user' => $res['full_name']]);
} else { } else {
http_response_code(401); http_response_code(401); echo json_encode(['error' => 'Login mislukt']);
echo json_encode(['error' => 'Inloggegevens onjuist']);
} }
exit; exit;
} }
// Auth check voor alle andere acties
if (!isset($_SESSION['user']) && $action !== 'login') { if (!isset($_SESSION['user']) && $action !== 'login') {
http_response_code(403); http_response_code(403); exit;
echo json_encode(['error' => 'Sessie verlopen']);
exit;
} }
// --- CLIENTS ---
$woocommerce = new Client($_ENV['WC_URL'], $_ENV['WC_KEY'], $_ENV['WC_SECRET'], ['version' => 'wc/v3', 'verify_ssl' => false, 'timeout' => 400]); $woocommerce = new Client($_ENV['WC_URL'], $_ENV['WC_KEY'], $_ENV['WC_SECRET'], ['version' => 'wc/v3', 'verify_ssl' => false, 'timeout' => 400]);
// --- 2. HELPERS --- // --- HELPERS ---
if ($action === 'get_payment_methods') { if ($action === 'get_payment_methods') {
try { try {
$gateways = $woocommerce->get('payment_gateways'); $gateways = $woocommerce->get('payment_gateways');
$output = []; $output = [];
foreach ($gateways as $gw) { foreach ($gateways as $gw) {
// Filter Apple Pay en Google Pay hier volledig weg if (str_contains($gw->id, 'applepay') || str_contains($gw->id, 'googlepay')) continue;
if (str_contains($gw->id, 'applepay') || str_contains($gw->id, 'googlepay') || str_contains($gw->id, 'riverty') || str_contains($gw->id, 'klarna')) {
continue;
}
if ($gw->enabled && (str_contains($gw->id, 'mollie') || str_contains($gw->id, 'riverty') || str_contains($gw->id, 'klarna'))) { if ($gw->enabled && (str_contains($gw->id, 'mollie') || str_contains($gw->id, 'riverty') || str_contains($gw->id, 'klarna'))) {
$output[] = ['id' => $gw->id, 'title' => $gw->method_title]; $output[] = ['id' => $gw->id, 'title' => $gw->method_title];
} }
} }
echo json_encode($output); echo json_encode($output);
} catch (Exception $e) { } catch (Exception $e) { echo json_encode([]); }
echo json_encode([]);
}
exit; exit;
} }
@ -88,108 +83,88 @@ if ($action === 'get_products') {
$enriched[] = $p; $enriched[] = $p;
} }
echo json_encode($enriched); echo json_encode($enriched);
} catch (Exception $e) { } catch (Exception $e) { echo json_encode([]); }
echo json_encode([]);
}
exit; exit;
} }
if ($action === 'postcode_check') { if ($action === 'postcode_check') {
$postcode = str_replace(' ', '', $_GET['postcode']); $postcode = str_replace(' ', '', $_GET['postcode']);
$url = "https://postcode.tech/api/v1/postcode?postcode={$postcode}&number=" . $_GET['number']; $url = "https://postcode.tech/api/v1/postcode?postcode={$postcode}&number=" . $_GET['number'];
$ch = curl_init($url); $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $_ENV['POSTCODE_TECH_KEY']]); curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $_ENV['POSTCODE_TECH_KEY']]);
echo curl_exec($ch); echo curl_exec($ch); exit;
exit;
} }
// --- 3. CREATE ORDER (V7.0 - INCL. 100 EURO THRESHOLD) --- // --- CREATE ORDER (V8.1 - MET LOGGING) ---
if ($action === 'create_order') { if ($action === 'create_order') {
$input = json_decode(file_get_contents('php://input'), true); $input = json_decode(file_get_contents('php://input'), true);
try { try {
$email = $input['billing']['email'];
$mediacode = $input['mediacode_internal'] ?? 'Geen'; $mediacode = $input['mediacode_internal'] ?? 'Geen';
$shipping_incl_tax = (float)($input['shipping_total'] ?? 0);
$wc_gateway_id = $input['payment_method']; $wc_gateway_id = $input['payment_method'];
$mollie_method = str_replace(['mollie_wc_gateway_', 'rve_'], '', $wc_gateway_id); $mollie_method = str_replace(['mollie_wc_gateway_', 'rve_'], '', $wc_gateway_id);
// 1. ACCOUNT SYNC
$existing_customers = $woocommerce->get('customers', ['email' => $email]);
$input['customer_id'] = !empty($existing_customers) ? $existing_customers[0]->id : 0;
// 2. SHIPPING TAX FIX
if ($shipping_incl_tax > 0) {
$shipping_ex_tax = $shipping_incl_tax / 1.21;
$input['shipping_lines'] = [['method_id' => 'flat_rate', 'method_title' => 'Verzendkosten', 'total' => number_format($shipping_ex_tax, 4, '.', '')]];
}
$input['payment_method'] = $wc_gateway_id; $input['payment_method'] = $wc_gateway_id;
$input['customer_note'] = "Agent: {$_SESSION['user']} | Mediacode: $mediacode"; $input['customer_note'] = "Agent: {$_SESSION['user']} | Mediacode: $mediacode";
// Attribution data voor herkomst-blokje // 3. ATTRIBUTION METADATA
$input['meta_data'][] = ['key' => '_wc_order_attribution_source_type', 'value' => 'utm']; $input['meta_data'][] = ['key' => '_wc_order_attribution_source_type', 'value' => 'utm'];
$input['meta_data'][] = ['key' => '_wc_order_attribution_utm_source', 'value' => 'SalesPanel']; $input['meta_data'][] = ['key' => '_wc_order_attribution_utm_source', 'value' => 'SalesPanel'];
$input['meta_data'][] = ['key' => '_wc_order_attribution_utm_campaign', 'value' => $mediacode]; $input['meta_data'][] = ['key' => '_wc_order_attribution_utm_campaign', 'value' => $mediacode];
$input['meta_data'][] = ['key' => '_wc_order_attribution_origin', 'value' => 'Sales Panel'];
$input['meta_data'][] = ['key' => 'Mediacode', 'value' => $mediacode]; $input['meta_data'][] = ['key' => 'Mediacode', 'value' => $mediacode];
// A. Order aanmaken in WooCommerce // ORDER AANMAKEN
$order = $woocommerce->post('orders', $input); $order = $woocommerce->post('orders', $input);
// EXTRA CHECK: Termijn betalingen pas vanaf 100 euro // 4. TERMIJN CHECK
if (in_array($mollie_method, ['in3', 'klarna', 'klarnapaylater', 'klarnasliceit']) && (float)$order->total < 100.00) { if (in_array($mollie_method, ['in3', 'klarna', 'klarnapaylater', 'klarnasliceit', 'riverty']) && (float)$order->total < 100.00) {
$woocommerce->delete("orders/{$order->id}", ['force' => true]); $woocommerce->delete("orders/{$order->id}", ['force' => true]);
throw new Exception("Termijnbetaling (in3/Klarna) pas mogelijk vanaf €100,-"); throw new Exception("Termijnbetaling pas vanaf €100,-");
} }
// 5. MOLLIE PAYMENT
$mollie = new MollieApiClient(); $mollie = new MollieApiClient();
$mollie->setApiKey($_ENV['MOLLIE_KEY']); $mollie->setApiKey($_ENV['MOLLIE_KEY']);
$is_sub = (stripos(json_encode($order->line_items), 'abonnement') !== false);
// B. Voorbereiden Mollie Data
$paymentData = [ $paymentData = [
"amount" => ["currency" => "EUR", "value" => ($mollie_method === 'ideal' && $is_sub) ? "0.01" : number_format((float)$order->total, 2, '.', '')], "amount" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')],
"description" => "Order #{$order->id}", "description" => "Order #{$order->id} [$mediacode]",
"redirectUrl" => $_ENV['WC_URL'] . "/checkout/order-received/{$order->id}/?key={$order->order_key}&order_id={$order->id}&utm_source=SalesPanel&utm_campaign={$mediacode}", "redirectUrl" => $_ENV['WC_URL'] . "/checkout/order-received/{$order->id}/?key={$order->order_key}&order_id={$order->id}&utm_source=SalesPanel&utm_campaign=" . urlencode($mediacode),
"webhookUrl" => $_ENV['WC_URL'] . "/wc-api/{$wc_gateway_id}/?key={$order->order_key}&order_id={$order->id}", "webhookUrl" => $_ENV['WC_URL'] . "/wc-api/{$wc_gateway_id}/?key={$order->order_key}&order_id={$order->id}",
"method" => $mollie_method, "method" => $mollie_method,
"metadata" => ["order_id" => (string)$order->id, "mediacode" => $mediacode] "metadata" => ["order_id" => (string)$order->id, "mediacode" => $mediacode]
]; ];
// C. "Light Lines" fix voor Server Error 500 / Klarna-validatie
if (in_array($mollie_method, ['in3', 'klarna', 'klarnapaylater', 'klarnasliceit', 'riverty'])) { if (in_array($mollie_method, ['in3', 'klarna', 'klarnapaylater', 'klarnasliceit', 'riverty'])) {
if ($mollie_method === 'riverty') { if ($mollie_method === 'riverty') $paymentData["captureMode"] = "manual";
$paymentData["captureMode"] = "manual"; $paymentData["billingAddress"] = ["givenName" => $input['billing']['first_name'], "familyName" => $input['billing']['last_name'], "email" => $input['billing']['email'], "streetAndNumber" => $input['billing']['address_1'], "city" => $input['billing']['city'], "postalCode" => $input['billing']['postcode'], "country" => "NL"];
} $paymentData["lines"] = [["name" => "Bestelling #" . $order->id, "quantity" => 1, "unitPrice" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')], "totalAmount" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')], "vatRate" => "21.00", "vatAmount" => ["currency" => "EUR", "value" => number_format((float)$order->total_tax, 2, '.', '')]]];
$paymentData["billingAddress"] = [
"givenName" => $input['billing']['first_name'],
"familyName" => $input['billing']['last_name'],
"email" => $input['billing']['email'],
"streetAndNumber" => $input['billing']['address_1'],
"city" => $input['billing']['city'],
"postalCode" => $input['billing']['postcode'],
"country" => "NL"
];
$paymentData["lines"] = [[
"name" => "Bestelling #" . $order->id,
"quantity" => 1,
"unitPrice" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')],
"totalAmount" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')],
"vatRate" => "21.00",
"vatAmount" => ["currency" => "EUR", "value" => number_format((float)$order->total_tax, 2, '.', '')]
]];
} }
// D. Mollie betaling aanmaken
$payment = $mollie->payments->create($paymentData); $payment = $mollie->payments->create($paymentData);
// E. Update Mollie ID in WooCommerce // 7. FINISH ORDER
$woocommerce->put("orders/{$order->id}", ['meta_data' => [['key' => '_mollie_payment_id', 'value' => $payment->id], ['key' => '_transaction_id', 'value' => $payment->id], ['key' => '_mediacode', 'value' => $mediacode]]]); $woocommerce->put("orders/{$order->id}", ['meta_data' => [['key' => '_mollie_payment_id', 'value' => $payment->id], ['key' => '_transaction_id', 'value' => $payment->id]]]);
$woocommerce->post("orders/{$order->id}/notes", ['note' => "Voltooi de betaling of keur de incasso z.s.m. goed via de volgende link: <br/> " . $payment->getCheckoutUrl(), 'customer_note' => true]);
$woocommerce->post("orders/{$order->id}/notes", [ writeLog('ORDER_CREATED', "Order #{$order->id} voor {$input['billing']['email']}");
'note' => "Betaallink gegenereerd: " . $payment->getCheckoutUrl(),
'customer_note' => true // Dit stuurt een mail naar de klant met de link!
]);
echo json_encode(['payment_url' => $payment->getCheckoutUrl()]); echo json_encode(['payment_url' => $payment->getCheckoutUrl()]);
} catch (Exception $e) { } catch (Exception $e) {
http_response_code(422); writeLog('ERROR', $e->getMessage());
echo json_encode(['error' => $e->getMessage()]); http_response_code(422); echo json_encode(['error' => $e->getMessage()]);
} }
exit; exit;
} }
if ($action === 'logout') { if ($action === 'logout') { session_destroy(); echo json_encode(['success' => true]); exit; }
session_destroy();
echo json_encode(['success' => true]);
exit;
}

View File

@ -5,14 +5,14 @@
<title>Telvero Sales Panel</title> <title>Telvero Sales Panel</title>
<script src="https://cdn.tailwindcss.com"></script> <script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/alpinejs" defer></script> <script src="https://unpkg.com/alpinejs" defer></script>
<style>[x-cloak] { display: none !important; }</style> <style>[x-cloak] { display: none !important; } .custom-scrollbar::-webkit-scrollbar { width: 4px; } .custom-scrollbar::-webkit-scrollbar-thumb { background: #334155; border-radius: 10px; }</style>
</head> </head>
<body class="bg-slate-100 min-h-screen font-sans" x-data="salesApp()"> <body class="bg-slate-100 min-h-screen font-sans" x-data="salesApp()">
<template x-if="!isLoggedIn"> <template x-if="!isLoggedIn">
<div class="fixed inset-0 bg-slate-900 flex items-center justify-center p-4 z-50"> <div class="fixed inset-0 bg-slate-900 flex items-center justify-center p-4 z-50">
<div class="bg-white p-10 rounded-[2.5rem] shadow-2xl w-full max-w-md text-center border-t-8 border-blue-600"> <div class="bg-white p-10 rounded-[2.5rem] shadow-2xl w-full max-w-md text-center border-t-8 border-blue-600">
<h2 class="text-3xl font-black mb-8 italic">TELVERO LOGIN</h2> <h2 class="text-3xl font-black mb-8 italic tracking-tighter text-slate-800">TELVERO <span class="text-blue-600">LOGIN</span></h2>
<div class="space-y-4"> <div class="space-y-4">
<input type="text" x-model="loginForm.username" placeholder="Gebruikersnaam" class="w-full border p-4 rounded-2xl outline-none focus:border-blue-500 bg-slate-50"> <input type="text" x-model="loginForm.username" placeholder="Gebruikersnaam" class="w-full border p-4 rounded-2xl outline-none focus:border-blue-500 bg-slate-50">
<input type="password" x-model="loginForm.password" @keyup.enter="doLogin()" placeholder="Wachtwoord" class="w-full border p-4 rounded-2xl outline-none focus:border-blue-500 bg-slate-50"> <input type="password" x-model="loginForm.password" @keyup.enter="doLogin()" placeholder="Wachtwoord" class="w-full border p-4 rounded-2xl outline-none focus:border-blue-500 bg-slate-50">
@ -24,7 +24,7 @@
<div x-show="isLoggedIn" x-cloak class="max-w-[1440px] mx-auto p-6"> <div x-show="isLoggedIn" x-cloak class="max-w-[1440px] mx-auto p-6">
<header class="flex justify-between items-center mb-8 bg-white p-6 rounded-3xl shadow-sm border-b-4 border-blue-600"> <header class="flex justify-between items-center mb-8 bg-white p-6 rounded-3xl shadow-sm border-b-4 border-blue-600">
<h1 class="text-2xl font-black italic">TELVERO <span class="text-blue-600">PANEL</span></h1> <h1 class="text-2xl font-black italic tracking-tighter">TELVERO <span class="text-blue-600">PANEL</span></h1>
<div class="flex items-center gap-6 text-sm font-bold text-slate-400"> <div class="flex items-center gap-6 text-sm font-bold text-slate-400">
<span x-text="'Agent: ' + currentUser"></span> <span x-text="'Agent: ' + currentUser"></span>
<button @click="doLogout()" class="text-red-500 underline uppercase text-xs font-black">Uitloggen</button> <button @click="doLogout()" class="text-red-500 underline uppercase text-xs font-black">Uitloggen</button>
@ -34,11 +34,11 @@
<div class="grid grid-cols-12 gap-8"> <div class="grid grid-cols-12 gap-8">
<div class="col-span-12 lg:col-span-4 bg-white p-8 rounded-[2rem] shadow-sm border border-slate-200"> <div class="col-span-12 lg:col-span-4 bg-white p-8 rounded-[2rem] shadow-sm border border-slate-200">
<div class="mb-8 p-6 bg-blue-50 rounded-2xl border-2 border-blue-100 shadow-inner"> <div class="mb-8 p-6 bg-blue-50 rounded-2xl border-2 border-blue-100 shadow-inner">
<label class="block text-[10px] font-black text-blue-600 uppercase tracking-widest mb-3 italic">Mediacode</label> <label class="block text-[10px] font-black text-blue-600 uppercase tracking-widest mb-3 italic text-center">Mediacode</label>
<select x-model="meta.mediacode" class="w-full border-2 border-white p-4 rounded-xl font-bold text-blue-800 shadow-sm outline-none focus:border-blue-300"> <select x-model="meta.mediacode" class="w-full border-2 border-white p-4 rounded-xl font-bold text-blue-800 shadow-sm outline-none focus:border-blue-300">
<option value="">-- KIES MEDIACODE --</option> <option value="">-- KIES MEDIACODE --</option>
<option value="TELVERO-NET5">TELVERO-NET5</option> <option value="Telvero - Net5">Telvero - Net5</option>
<option value="TELVERO-SBS6">TELVERO-SBS6</option> <option value="Telvero - SBS9">Telvero - SBS9</option>
</select> </select>
</div> </div>
<div class="space-y-4"> <div class="space-y-4">
@ -51,15 +51,15 @@
<input type="text" x-model="form.houseno" @blur="lookupAddress()" placeholder="Nr." class="border p-3 rounded-xl w-full"> <input type="text" x-model="form.houseno" @blur="lookupAddress()" placeholder="Nr." class="border p-3 rounded-xl w-full">
<input type="text" x-model="form.suffix" placeholder="Toev." class="border p-3 rounded-xl w-full"> <input type="text" x-model="form.suffix" placeholder="Toev." class="border p-3 rounded-xl w-full">
</div> </div>
<input type="text" x-model="form.street" placeholder="Straat" class="w-full border p-3 rounded-xl bg-slate-100 font-bold text-xs shadow-inner" readonly> <input type="text" x-model="form.street" placeholder="Straat" class="w-full border p-3 rounded-xl bg-slate-100 font-bold text-xs" readonly>
<input type="text" x-model="form.city" placeholder="Stad" class="w-full border p-3 rounded-xl bg-slate-100 font-bold text-xs shadow-inner" readonly> <input type="text" x-model="form.city" placeholder="Stad" class="w-full border p-3 rounded-xl bg-slate-100 font-bold text-xs" readonly>
<input type="tel" x-model="form.phone" placeholder="Telefoon (06...)" class="border p-3 rounded-xl w-full focus:border-blue-500 outline-none"> <input type="tel" x-model="form.phone" placeholder="Telefoon (06...)" class="border p-3 rounded-xl w-full focus:border-blue-500 outline-none">
<input type="email" x-model="form.email" placeholder="E-mail (Verplicht)" class="border-2 border-amber-300 p-3 rounded-xl w-full outline-none focus:border-amber-500 shadow-sm"> <input type="email" x-model="form.email" placeholder="E-mail (Verplicht)" class="border-2 border-amber-300 p-3 rounded-xl w-full outline-none focus:border-amber-500 shadow-sm">
</div> </div>
</div> </div>
<div class="col-span-12 lg:col-span-5 bg-white p-8 rounded-[2rem] shadow-sm border border-slate-200"> <div class="col-span-12 lg:col-span-5 bg-white p-8 rounded-[2rem] shadow-sm border border-slate-200">
<h2 class="font-bold mb-6 text-slate-400 uppercase text-[10px] tracking-widest border-b pb-2 text-center italic">Selectie</h2> <h2 class="font-bold mb-4 text-slate-400 uppercase text-[10px] tracking-widest border-b pb-2 text-center italic">Product Selecteren</h2>
<select x-model="selectedProductId" @change="selectProduct()" class="w-full border-2 border-slate-100 p-5 rounded-2xl font-black text-slate-700 mb-6 bg-slate-50 outline-none focus:border-blue-500 shadow-sm"> <select x-model="selectedProductId" @change="selectProduct()" class="w-full border-2 border-slate-100 p-5 rounded-2xl font-black text-slate-700 mb-6 bg-slate-50 outline-none focus:border-blue-500 shadow-sm">
<option value="">-- Kies Hoofdproduct --</option> <option value="">-- Kies Hoofdproduct --</option>
<template x-for="p in products" :key="p.id"> <template x-for="p in products" :key="p.id">
@ -68,21 +68,33 @@
</select> </select>
<div x-show="variations.length > 0" x-cloak class="mb-8 p-6 bg-blue-50 rounded-3xl border border-blue-100 shadow-inner text-center"> <div x-show="variations.length > 0" x-cloak class="mb-8 p-6 bg-blue-50 rounded-3xl border border-blue-100 shadow-inner text-center">
<label class="block text-[10px] font-black text-blue-600 uppercase mb-3 tracking-widest italic">Kies Optie</label> <select x-model="selectedVariationId" @change="selectVariation()" class="w-full border-2 border-white p-4 rounded-2xl font-bold bg-white text-slate-700 shadow-sm outline-none">
<select x-model="selectedVariationId" @change="selectVariation()" class="w-full border-2 border-white p-4 rounded-2xl font-bold bg-white text-slate-700 shadow-sm"> <option value="">-- Kies Optie --</option>
<option value="">-- Maak een keuze --</option>
<template x-for="v in variations" :key="v.id"> <template x-for="v in variations" :key="v.id">
<option :value="v.id" x-text="getVarName(v) + ' (€' + v.price + ')'"></option> <option :value="v.id" x-text="getVarName(v) + ' (€' + v.price + ')'"></option>
</template> </template>
</select> </select>
</div> </div>
<div x-show="upsellOptions.length > 0" x-cloak class="space-y-3"> <div class="mt-8 border-t pt-6 text-center">
<h2 class="font-bold mb-4 text-slate-400 uppercase text-[10px] tracking-widest italic">Extra Product Toevoegen</h2>
<div class="flex gap-2">
<select x-model="extraProductId" class="flex-1 border-2 border-slate-100 p-3 rounded-xl font-bold text-xs bg-slate-50 outline-none focus:border-green-500">
<option value="">-- Kies extra product --</option>
<template x-for="p in products" :key="'extra-'+p.id">
<option :value="p.id" x-text="p.name + ' (€' + p.price + ')'"></option>
</template>
</select>
<button @click="addExtraItem()" class="bg-green-600 text-white px-6 py-3 rounded-xl font-black text-[10px] uppercase shadow-md hover:bg-green-700 transition active:scale-95">Toevoegen</button>
</div>
</div>
<div x-show="upsellOptions.length > 0" x-cloak class="mt-8 space-y-3">
<p class="text-[10px] font-black text-red-500 uppercase tracking-widest italic px-2 text-center">Upsell Suggesties</p> <p class="text-[10px] font-black text-red-500 uppercase tracking-widest italic px-2 text-center">Upsell Suggesties</p>
<template x-for="u in upsellOptions" :key="u.id"> <template x-for="u in upsellOptions" :key="u.id">
<div class="flex items-center justify-between p-4 border rounded-2xl bg-slate-50 hover:bg-white transition-all shadow-sm border-slate-100"> <div class="flex items-center justify-between p-4 border rounded-2xl bg-slate-50 hover:bg-white transition-all shadow-sm">
<span class="text-xs font-bold text-slate-700" x-text="u.name + ' (€' + u.price + ')'"></span> <span class="text-xs font-bold text-slate-700" x-text="u.name + ' (€' + u.price + ')'"></span>
<button @click="toggleUpsell(u)" :class="isInCart(u.id) ? 'bg-red-500' : 'bg-green-600'" class="text-white px-6 py-2 rounded-xl text-[10px] font-black uppercase shadow-md transition active:scale-95" x-text="isInCart(u.id) ? 'Verwijder' : 'Voeg toe'"></button> <button @click="toggleUpsell(u)" :class="isInCart(u.id) ? 'bg-red-500' : 'bg-green-600'" class="text-white px-6 py-2 rounded-xl text-[10px] font-black uppercase shadow-md" x-text="isInCart(u.id) ? 'Verwijder' : 'Voeg toe'"></button>
</div> </div>
</template> </template>
</div> </div>
@ -90,29 +102,34 @@
<div class="col-span-12 lg:col-span-3"> <div class="col-span-12 lg:col-span-3">
<div class="bg-slate-900 text-white p-8 rounded-[2.5rem] shadow-2xl sticky top-6 border border-slate-800"> <div class="bg-slate-900 text-white p-8 rounded-[2.5rem] shadow-2xl sticky top-6 border border-slate-800">
<h2 class="font-bold mb-6 border-b border-slate-800 pb-2 text-[10px] uppercase text-slate-500 tracking-widest italic text-center">Winkelmand</h2> <h2 class="font-bold mb-6 border-b border-slate-800 pb-2 text-[10px] uppercase text-slate-500 tracking-widest italic text-center">Overzicht</h2>
<div class="space-y-4 mb-8 min-h-[100px] max-h-[300px] overflow-y-auto pr-2 custom-scrollbar"> <div class="space-y-4 mb-6 min-h-[80px] max-h-[250px] overflow-y-auto pr-2 custom-scrollbar">
<template x-for="(item, index) in cart" :key="index"> <template x-for="(item, index) in cart" :key="index">
<div class="flex justify-between items-center group"> <div class="flex justify-between items-center group">
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<span x-text="item.name" class="text-[11px] font-medium leading-tight text-slate-300"></span> <span x-text="item.name" class="text-[11px] font-medium leading-tight text-slate-300"></span>
<span x-text="'€' + item.price" class="text-[11px] font-black text-blue-400"></span> <span x-text="'€' + item.price" class="text-[11px] font-black text-blue-400"></span>
</div> </div>
<button @click="removeFromCart(index)" class="text-slate-600 hover:text-red-500 transition-colors p-1"> <button @click="removeFromCart(index)" class="text-slate-600 hover:text-red-500">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg> <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
</button> </button>
</div> </div>
</template> </template>
</div> </div>
<div class="mb-8 pt-4 border-t border-slate-800 space-y-2 text-center"> <div class="mb-6 pt-4 border-t border-slate-800 text-center">
<label class="text-[10px] text-slate-500 uppercase font-black mb-2 block italic tracking-widest">Verzendkosten (€)</label>
<input type="number" x-model="shipping" step="0.01" min="0" class="w-full bg-slate-800 border border-slate-700 rounded-xl p-2 text-center text-sm font-bold text-blue-400 outline-none focus:border-blue-500">
</div>
<div class="mb-8 pt-4 border-t border-slate-800 text-center">
<p class="text-[10px] text-slate-500 uppercase font-black mb-3 italic tracking-widest">Betaalwijze</p> <p class="text-[10px] text-slate-500 uppercase font-black mb-3 italic tracking-widest">Betaalwijze</p>
<div class="space-y-2 max-h-[250px] overflow-y-auto pr-2 custom-scrollbar"> <div class="space-y-2 max-h-[180px] overflow-y-auto pr-2 custom-scrollbar">
<template x-for="method in filteredPaymentMethods" :key="method.id"> <template x-for="method in filteredPaymentMethods" :key="method.id">
<button @click="payment_method = method.id" <button @click="payment_method = method.id"
:class="payment_method === method.id ? 'bg-blue-600 border-blue-400' : 'bg-slate-800 border-slate-700 opacity-60'" :class="payment_method === method.id ? 'bg-blue-600 border-blue-400 ring-2 ring-blue-500/50' : 'bg-slate-800 border-slate-700 opacity-60'"
class="w-full text-left p-3 rounded-2xl border flex items-center gap-3 transition-all duration-200"> class="w-full text-left p-3 rounded-2xl border flex items-center gap-3 transition-all duration-200">
<img :src="method.image" class="w-7 h-7 rounded bg-white p-1 shadow-inner"> <img :src="method.image" class="w-7 h-7 rounded bg-white p-1">
<span class="text-[10px] font-black uppercase text-white tracking-tighter" x-text="method.title"></span> <span class="text-[10px] font-black uppercase text-white tracking-tighter" x-text="method.title"></span>
</button> </button>
</template> </template>
@ -139,8 +156,9 @@
isLoggedIn: false, currentUser: '', loginForm: { username: '', password: '' }, isLoggedIn: false, currentUser: '', loginForm: { username: '', password: '' },
products: [], paymentMethods: [], upsellOptions: [], cart: [], activeProduct: null, products: [], paymentMethods: [], upsellOptions: [], cart: [], activeProduct: null,
selectedProductId: '', selectedVariationId: '', variations: [], payment_method: '', selectedProductId: '', selectedVariationId: '', variations: [], payment_method: '',
extraProductId: '', shipping: '9.95',
submitting: false, submitting: false,
form: { initials: '', lastname: '', postcode: '', houseno: '', suffix: '', street: '', city: '', email: '', dob: '', phone: '' }, form: { initials: '', lastname: '', postcode: '', houseno: '', suffix: '', street: '', city: '', email: '', phone: '' },
meta: { mediacode: '' }, meta: { mediacode: '' },
async doLogin() { async doLogin() {
@ -166,15 +184,13 @@
if (m.id.includes('in3')) iconKey = 'in3'; if (m.id.includes('in3')) iconKey = 'in3';
return { ...m, image: `https://www.mollie.com/external/icons/payment-methods/${iconKey}.svg` }; return { ...m, image: `https://www.mollie.com/external/icons/payment-methods/${iconKey}.svg` };
}); });
if(this.paymentMethods.length > 0) this.payment_method = 'mollie_wc_gateway_ideal'; this.payment_method = 'mollie_wc_gateway_ideal';
}, },
// 100 EURO DREMPEL LOGICA
get filteredPaymentMethods() { get filteredPaymentMethods() {
return this.paymentMethods.filter(method => { return this.paymentMethods.filter(method => {
const isTermin = method.id.includes('in3') || method.id.includes('klarna'); const isTermin = method.id.includes('in3') || method.id.includes('klarna') || method.id.includes('riverty');
const currentTotal = parseFloat(this.total); if (isTermin && parseFloat(this.total) < 100) {
if (isTermin && currentTotal < 100) {
if (this.payment_method === method.id) this.payment_method = 'mollie_wc_gateway_ideal'; if (this.payment_method === method.id) this.payment_method = 'mollie_wc_gateway_ideal';
return false; return false;
} }
@ -200,6 +216,13 @@
this.loadUpsells(this.activeProduct); this.loadUpsells(this.activeProduct);
}, },
addExtraItem() {
const p = this.products.find(x => x.id == this.extraProductId);
if(!p) return;
this.cart.push({ id: parseInt(p.id), name: p.name, price: p.price });
this.extraProductId = '';
},
loadUpsells(product) { loadUpsells(product) {
this.upsellOptions = []; this.upsellOptions = [];
if (product.upsell_ids && product.upsell_ids.length > 0) { if (product.upsell_ids && product.upsell_ids.length > 0) {
@ -216,23 +239,27 @@
else { this.cart.push({ id: parseInt(u.id), name: u.name, price: u.price }); } else { this.cart.push({ id: parseInt(u.id), name: u.name, price: u.price }); }
}, },
isInCart(id) { return this.cart.some(i => parseInt(i.id) === parseInt(id)); }, isInCart(id) { return this.cart.some(i => parseInt(i.id) === parseInt(id)); },
get total() { return this.cart.reduce((sum, item) => sum + parseFloat(item.price), 0).toFixed(2); },
get total() {
const itemsTotal = this.cart.reduce((sum, item) => sum + parseFloat(item.price), 0);
return (itemsTotal + (parseFloat(this.shipping) || 0)).toFixed(2);
},
formatInitials() { let v = this.form.initials.replace(/[^a-z]/gi, '').toUpperCase(); this.form.initials = v.split('').join('.') + (v ? '.' : ''); }, formatInitials() { let v = this.form.initials.replace(/[^a-z]/gi, '').toUpperCase(); this.form.initials = v.split('').join('.') + (v ? '.' : ''); },
formatLastname() { this.form.lastname = this.form.lastname.charAt(0).toUpperCase() + this.form.lastname.slice(1); }, formatLastname() { this.form.lastname = this.form.lastname.charAt(0).toUpperCase() + this.form.lastname.slice(1); },
formatDOB() { let d = this.form.dob.replace(/[^0-9]/g, ''); if(d.length === 8) this.form.dob = `${d.slice(0,2)}-${d.slice(2,4)}-${d.slice(4,8)}`; },
async submitOrder() { async submitOrder() {
this.submitting = true; this.submitting = true;
const payload = { const payload = {
payment_method: this.payment_method, mediacode_internal: this.meta.mediacode, payment_method: this.payment_method, mediacode_internal: this.meta.mediacode,
shipping_total: this.shipping,
billing: { first_name: this.form.initials, last_name: this.form.lastname, address_1: (this.form.street + ' ' + this.form.houseno + ' ' + (this.form.suffix || '')).trim(), city: this.form.city, postcode: this.form.postcode, country: 'NL', email: this.form.email, phone: this.form.phone }, billing: { first_name: this.form.initials, last_name: this.form.lastname, address_1: (this.form.street + ' ' + this.form.houseno + ' ' + (this.form.suffix || '')).trim(), city: this.form.city, postcode: this.form.postcode, country: 'NL', email: this.form.email, phone: this.form.phone },
line_items: this.cart.map(i => ({ product_id: i.id, variation_id: i.variation_id || 0, quantity: 1 })) line_items: this.cart.map(i => ({ product_id: i.id, variation_id: i.variation_id || 0, quantity: 1 }))
}; };
try { try {
const res = await fetch('api.php?action=create_order', { method: 'POST', body: JSON.stringify(payload) }); const res = await fetch('api.php?action=create_order', { method: 'POST', body: JSON.stringify(payload) });
const result = await res.json(); const result = await res.json();
if(result.payment_url) { alert("Succes! Order aangemaakt."); this.cart = []; this.selectedProductId = ''; this.form = { initials: '', lastname: '', postcode: '', houseno: '', suffix: '', street: '', city: '', email: '', dob: '', phone: '' }; } if(result.payment_url) { alert("Succes!"); this.cart = []; this.selectedProductId = ''; this.shipping = '9.95'; this.form = { initials: '', lastname: '', postcode: '', houseno: '', suffix: '', street: '', city: '', email: '', phone: '' }; }
else { alert("Fout: " + result.error); } else { alert("Fout: " + result.error); }
} catch(e) { alert("Systeemfout"); } } catch(e) { alert("Systeemfout"); }
this.submitting = false; this.submitting = false;

200
logs.php
View File

@ -1,63 +1,163 @@
<?php <?php
/** /**
* TELVERO LOG VIEWER (ENV VERSION) * TELVERO BACKOFFICE - API PROXY (V8.1 - LOGGING RESTORED)
*/ */
session_start(); session_start();
ini_set('display_errors', 0);
error_reporting(E_ALL);
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/vendor/autoload.php';
// Laad .env configuratie if (file_exists(__DIR__ . '/.env')) {
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__); $dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load(); $dotenv->load();
if (!isset($_SESSION['user'])) {
die("Toegang geweigerd. Log eerst in via het dashboard.");
} }
// Database connectie via ENV use Automattic\WooCommerce\Client;
use Mollie\Api\MollieApiClient;
header('Content-Type: application/json');
$db = new mysqli($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASS'], $_ENV['DB_NAME']); $db = new mysqli($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASS'], $_ENV['DB_NAME']);
if ($db->connect_error) { die(json_encode(['error' => 'Database connectie mislukt'])); }
if ($db->connect_error) { $action = $_GET['action'] ?? '';
die("Database connectie mislukt.");
// --- AUTH ---
if ($action === 'login') {
$input = json_decode(file_get_contents('php://input'), true);
$stmt = $db->prepare("SELECT password, full_name FROM sales_users WHERE username = ?");
$stmt->bind_param("s", $input['username']);
$stmt->execute();
$res = $stmt->get_result()->fetch_assoc();
if ($res && password_verify($input['password'], $res['password'])) {
$_SESSION['user'] = $input['username'];
$_SESSION['full_name'] = $res['full_name'];
session_write_close();
echo json_encode(['success' => true, 'user' => $res['full_name']]);
} else {
http_response_code(401); echo json_encode(['error' => 'Login mislukt']);
}
exit;
} }
$result = $db->query("SELECT * FROM sales_logs ORDER BY created_at DESC LIMIT 100"); if (!isset($_SESSION['user']) && $action !== 'login') {
?> http_response_code(403); exit;
<!DOCTYPE html> }
<html lang="nl">
<head> $woocommerce = new Client($_ENV['WC_URL'], $_ENV['WC_KEY'], $_ENV['WC_SECRET'], ['version' => 'wc/v3', 'verify_ssl' => false, 'timeout' => 400]);
<meta charset="UTF-8">
<title>Telvero Logs</title> // --- HELPERS ---
<script src="https://cdn.tailwindcss.com"></script> if ($action === 'get_payment_methods') {
</head> try {
<body class="bg-slate-50 p-8 font-sans"> $gateways = $woocommerce->get('payment_gateways');
<div class="max-w-5xl mx-auto"> $output = [];
<div class="flex justify-between items-center mb-10"> foreach ($gateways as $gw) {
<h1 class="text-3xl font-black italic text-slate-800">SALES <span class="text-blue-600">AUDIT LOGS</span></h1> if (str_contains($gw->id, 'applepay') || str_contains($gw->id, 'googlepay')) continue;
<a href="index.html" class="bg-white px-6 py-2 rounded-xl shadow-sm font-bold text-sm text-blue-600 border border-blue-100 hover:bg-blue-50 transition">Dashboard</a> if ($gw->enabled && (str_contains($gw->id, 'mollie') || str_contains($gw->id, 'riverty') || str_contains($gw->id, 'klarna'))) {
</div> $output[] = ['id' => $gw->id, 'title' => $gw->method_title];
}
}
echo json_encode($output);
} catch (Exception $e) { echo json_encode([]); }
exit;
}
if ($action === 'get_products') {
try {
$products = $woocommerce->get('products', ['status' => 'publish', 'per_page' => 100]);
$enriched = [];
foreach ($products as $product) {
$p = (array)$product;
$p['variation_details'] = ($product->type === 'variable') ? (array)$woocommerce->get("products/{$product->id}/variations", ['per_page' => 50]) : [];
$enriched[] = $p;
}
echo json_encode($enriched);
} catch (Exception $e) { echo json_encode([]); }
exit;
}
if ($action === 'postcode_check') {
$postcode = str_replace(' ', '', $_GET['postcode']);
$url = "https://postcode.tech/api/v1/postcode?postcode={$postcode}&number=" . $_GET['number'];
$ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Authorization: Bearer " . $_ENV['POSTCODE_TECH_KEY']]);
echo curl_exec($ch); exit;
}
// --- CREATE ORDER (V8.1 - MET LOGGING) ---
if ($action === 'create_order') {
$input = json_decode(file_get_contents('php://input'), true);
try {
$email = $input['billing']['email'];
$mediacode = $input['mediacode_internal'] ?? 'Geen';
$shipping_incl_tax = (float)($input['shipping_total'] ?? 0);
$wc_gateway_id = $input['payment_method'];
$mollie_method = str_replace(['mollie_wc_gateway_', 'rve_'], '', $wc_gateway_id);
// 1. ACCOUNT SYNC
$existing_customers = $woocommerce->get('customers', ['email' => $email]);
$input['customer_id'] = !empty($existing_customers) ? $existing_customers[0]->id : 0;
// 2. SHIPPING TAX FIX
if ($shipping_incl_tax > 0) {
$shipping_ex_tax = $shipping_incl_tax / 1.21;
$input['shipping_lines'] = [['method_id' => 'flat_rate', 'method_title' => 'Verzendkosten', 'total' => number_format($shipping_ex_tax, 4, '.', '')]];
}
$input['payment_method'] = $wc_gateway_id;
$input['customer_note'] = "Agent: {$_SESSION['user']} | Mediacode: $mediacode";
// 3. ATTRIBUTION METADATA
$input['meta_data'][] = ['key' => '_wc_order_attribution_source_type', 'value' => 'utm'];
$input['meta_data'][] = ['key' => '_wc_order_attribution_utm_source', 'value' => 'SalesPanel'];
$input['meta_data'][] = ['key' => '_wc_order_attribution_utm_campaign', 'value' => $mediacode];
$input['meta_data'][] = ['key' => 'Mediacode', 'value' => $mediacode];
// ORDER AANMAKEN
$order = $woocommerce->post('orders', $input);
// 4. TERMIJN CHECK
if (in_array($mollie_method, ['in3', 'klarna', 'klarnapaylater', 'klarnasliceit', 'riverty']) && (float)$order->total < 100.00) {
$woocommerce->delete("orders/{$order->id}", ['force' => true]);
throw new Exception("Termijnbetaling pas vanaf €100,-");
}
// 5. MOLLIE PAYMENT
$mollie = new MollieApiClient();
$mollie->setApiKey($_ENV['MOLLIE_KEY']);
<div class="bg-white rounded-[2rem] shadow-xl overflow-hidden border border-slate-100"> $paymentData = [
<table class="w-full text-left border-collapse"> "amount" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')],
<thead> "description" => "Order #{$order->id} [$mediacode]",
<tr class="bg-slate-50 border-b border-slate-100 text-[10px] uppercase tracking-[0.2em] text-slate-400 font-black"> "redirectUrl" => $_ENV['WC_URL'] . "/checkout/order-received/{$order->id}/?key={$order->order_key}&order_id={$order->id}&utm_source=SalesPanel&utm_campaign=" . urlencode($mediacode),
<th class="p-6">Tijdstip</th> "webhookUrl" => $_ENV['WC_URL'] . "/wc-api/{$wc_gateway_id}/?key={$order->order_key}&order_id={$order->id}",
<th class="p-6">Agent</th> "method" => $mollie_method,
<th class="p-6">Actie</th> "metadata" => ["order_id" => (string)$order->id, "mediacode" => $mediacode]
<th class="p-6">Omschrijving</th> ];
</tr>
</thead> if (in_array($mollie_method, ['in3', 'klarna', 'klarnapaylater', 'klarnasliceit', 'riverty'])) {
<tbody class="text-sm divide-y divide-slate-50"> if ($mollie_method === 'riverty') $paymentData["captureMode"] = "manual";
<?php while($row = $result->fetch_assoc()): ?> $paymentData["billingAddress"] = ["givenName" => $input['billing']['first_name'], "familyName" => $input['billing']['last_name'], "email" => $input['billing']['email'], "streetAndNumber" => $input['billing']['address_1'], "city" => $input['billing']['city'], "postalCode" => $input['billing']['postcode'], "country" => "NL"];
<tr class="hover:bg-blue-50/30 transition-colors"> $paymentData["lines"] = [["name" => "Bestelling #" . $order->id, "quantity" => 1, "unitPrice" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')], "totalAmount" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')], "vatRate" => "21.00", "vatAmount" => ["currency" => "EUR", "value" => number_format((float)$order->total_tax, 2, '.', '')]]];
<td class="p-6 text-xs font-mono text-slate-400"><?= $row['created_at'] ?></td> }
<td class="p-6 font-black text-slate-700 italic underline decoration-blue-500/30"><?= htmlspecialchars($row['username']) ?></td>
<td class="p-6"><span class="px-2 py-1 bg-blue-100 text-blue-600 rounded text-[10px] font-black uppercase"><?= $row['action'] ?></span></td> $payment = $mollie->payments->create($paymentData);
<td class="p-6 text-slate-500 italic"><?= htmlspecialchars($row['details']) ?></td>
</tr> // 6. LOGGING NAAR DB (HERSTELD)
<?php endwhile; ?> $log_stmt = $db->prepare("INSERT INTO sales_logs (agent_user, order_id, amount, mediacode, customer_email) VALUES (?, ?, ?, ?, ?)");
</tbody> $log_stmt->bind_param("sidss", $_SESSION['user'], $order->id, $order->total, $mediacode, $email);
</table> $log_stmt->execute();
</div>
</div> // 7. FINISH ORDER
</body> $woocommerce->put("orders/{$order->id}", ['meta_data' => [['key' => '_mollie_payment_id', 'value' => $payment->id], ['key' => '_transaction_id', 'value' => $payment->id]]]);
</html> $woocommerce->post("orders/{$order->id}/notes", ['note' => "Betaallink gegenereerd: " . $payment->getCheckoutUrl(), 'customer_note' => true]);
echo json_encode(['payment_url' => $payment->getCheckoutUrl()]);
} catch (Exception $e) {
http_response_code(422); echo json_encode(['error' => $e->getMessage()]);
}
exit;
}
if ($action === 'logout') { session_destroy(); echo json_encode(['success' => true]); exit; }