diff --git a/api.php b/api.php index 3efca87..d32392a 100644 --- a/api.php +++ b/api.php @@ -1,49 +1,86 @@ $duration, + 'path' => '/', + 'secure' => isset($_SERVER['HTTPS']), + 'httponly' => true, + 'samesite' => 'Lax' +]); + +if (session_status() === PHP_SESSION_NONE) { + session_start(); +} + +// 2. RECOVERY LOGIC (Check cookie before WP loads) +if (!isset($_SESSION['user']) && isset($_COOKIE['telvero_remember'])) { + $decoded = json_decode(base64_decode($_COOKIE['telvero_remember']), true); + if ($decoded && $decoded['expires'] > time()) { + $_SESSION['user'] = $decoded['user']; + $_SESSION['full_name'] = $decoded['full_name']; + } +} + +// 3. CAPTURE DATA FOR WP PROTECTION +$cap_user = $_SESSION['user'] ?? null; +$cap_name = $_SESSION['full_name'] ?? null; + +// 4. LOAD WORDPRESS +$wp_load = __DIR__ . '/wp-load.php'; +if (!file_exists($wp_load)) { $wp_load = dirname(__DIR__) . '/wp-load.php'; } +if (file_exists($wp_load)) { require_once $wp_load; } + +// 5. RESTORE DATA +if ($cap_user && !isset($_SESSION['user'])) { + $_SESSION['user'] = $cap_user; + $_SESSION['full_name'] = $cap_name; +} + +// 6. REST OF THE BOOTSTRAP require __DIR__ . '/vendor/autoload.php'; - if (file_exists(__DIR__ . '/.env')) { $dotenv = Dotenv\Dotenv::createImmutable(__DIR__); $dotenv->load(); } 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']); if ($db->connect_error) { 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'] ?? ''; -// --- AUTH --- +// 6. LOGIN ACTION if ($action === 'login') { $input = json_decode(file_get_contents('php://input'), true); + $username = $input['username'] ?? ''; $stmt = $db->prepare("SELECT password, full_name FROM sales_users WHERE username = ?"); - $stmt->bind_param("s", $input['username']); + $stmt->bind_param("s", $username); $stmt->execute(); $res = $stmt->get_result()->fetch_assoc(); + if ($res && password_verify($input['password'], $res['password'])) { - $_SESSION['user'] = $input['username']; + $_SESSION['user'] = $username; $_SESSION['full_name'] = $res['full_name']; - writeLog('LOGIN', 'Gebruiker ingelogd'); - session_write_close(); + + // Recovery cookie payload + $cookie_payload = base64_encode(json_encode([ + 'user' => $username, + 'full_name' => $res['full_name'], + 'expires' => $midnight_timestamp + ])); + + // Use the explicit $midnight_timestamp variable + setcookie('telvero_remember', $cookie_payload, $midnight_timestamp, '/', '', isset($_SERVER['HTTPS']), true); + echo json_encode(['success' => true, 'user' => $res['full_name']]); } else { http_response_code(401); echo json_encode(['error' => 'Login mislukt']); @@ -51,117 +88,417 @@ if ($action === 'login') { exit; } -if (!isset($_SESSION['user']) && $action !== 'login') { - http_response_code(403); exit; +// 7. SECURITY GATE (The ONLY one needed) +if (!isset($_SESSION['user'])) { + http_response_code(403); + echo json_encode(['error' => 'Not authenticated']); + exit; } -$woocommerce = new Client($_ENV['WC_URL'], $_ENV['WC_KEY'], $_ENV['WC_SECRET'], ['version' => 'wc/v3', 'verify_ssl' => false, 'timeout' => 400]); +// 8. LOGOUT +if ($action === 'logout') { + session_destroy(); + setcookie('telvero_remember', '', time() - 3600, '/'); + echo json_encode(['success' => true]); + exit; +} -// --- HELPERS --- -if ($action === 'get_payment_methods') { - try { - $gateways = $woocommerce->get('payment_gateways'); - $output = []; - foreach ($gateways as $gw) { - if (str_contains($gw->id, 'applepay') || str_contains($gw->id, 'googlepay')) continue; - 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]; +$woocommerce = new Client($_ENV['WC_URL'], $_ENV['WC_KEY'], $_ENV['WC_SECRET'], ['version' => 'wc/v3', 'verify_ssl' => false]); + +function ss_get_upsellwp_recommended_product_ids($trigger_product_id) { + if (!function_exists('get_option')) { + return []; + } + + global $wpdb; + + $trigger_product_id = (int) $trigger_product_id; + if ($trigger_product_id <= 0) return []; + + // 1) Vind "campaign posts" waar in meta_value ergens dit product_id voorkomt. + // We weten niet exact welke meta_key UpsellWP gebruikt, dus zoeken we breed op meta_key LIKE '%upsellwp%'. + // Daarnaast zoeken we op bekende patronen in serialized/json/plain. + $like_json = '%' . $wpdb->esc_like('"' . $trigger_product_id . '"') . '%'; + $like_plain = '%' . $wpdb->esc_like((string) $trigger_product_id) . '%'; + $like_serial = '%' . $wpdb->esc_like('i:' . $trigger_product_id . ';') . '%'; + + $campaign_post_ids = $wpdb->get_col( + $wpdb->prepare( + " + SELECT DISTINCT post_id + FROM {$wpdb->postmeta} + WHERE meta_key LIKE %s + AND ( + meta_value LIKE %s + OR meta_value LIKE %s + OR meta_value LIKE %s + ) + ", + '%upsellwp%', + $like_json, + $like_serial, + $like_plain + ) + ); + + if (empty($campaign_post_ids)) { + return []; + } + + // 2) Verzamel uit die campaigns alle product IDs die genoemd worden (offers / bundles / buy-more-save-more tiers). + $recommended = []; + + foreach ($campaign_post_ids as $cid) { + $cid = (int) $cid; + + $rows = $wpdb->get_results( + $wpdb->prepare( + " + SELECT meta_key, meta_value + FROM {$wpdb->postmeta} + WHERE post_id = %d + ", + $cid + ) + ); + + foreach ($rows as $r) { + $key = (string) $r->meta_key; + + // Kleine bias naar keys die logisch product/offers bevatten + // maar alsnog niet te streng, want add-ons gebruiken soms andere keys. + if (!preg_match('/product|products|offer|offers|bundle|bmsm|tier|rule|condition|trigger|cart|recommend/i', $key) + && stripos($key, 'upsellwp') === false + ) { + continue; + } + + $val = (string) $r->meta_value; + + // Extract alle integers uit json/serialized/plain + if (preg_match_all('/\b\d+\b/', $val, $m)) { + foreach ($m[0] as $id) { + $id = (int) $id; + if ($id > 0) $recommended[] = $id; + } } } - echo json_encode($output); - } catch (Exception $e) { echo json_encode([]); } - exit; + } + + // 3) Opschonen: unieke IDs, trigger zelf eruit, en alleen echte WC producten (best effort). + $recommended = array_values(array_unique(array_filter($recommended))); + $recommended = array_values(array_diff($recommended, [$trigger_product_id])); + + // Filter op bestaande producten (scheelt false positives zoals campaign IDs) + $recommended = array_values(array_filter($recommended, function($id){ + return get_post_type($id) === 'product' || get_post_type($id) === 'product_variation'; + })); + + return $recommended; } -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; +/** + * Build a map: product_id => related product ids from CUW (UpsellWP) campaigns. + * Uses {$wpdb->prefix}cuw_campaigns table (as in your screenshot). + */ +function ss_cuw_build_product_map() { + if (!function_exists('get_post_type')) return []; + + global $wpdb; + + $table = $wpdb->prefix . 'cuw_campaigns'; + // Safety: table might not exist + $exists = $wpdb->get_var( $wpdb->prepare("SHOW TABLES LIKE %s", $table) ); + if ($exists !== $table) return []; + + $rows = $wpdb->get_results(" + SELECT id, type, filters, data + FROM {$table} + WHERE enabled = 1 + ORDER BY priority DESC, id DESC + "); + + if (empty($rows)) return []; + + $map = []; + + foreach ($rows as $row) { + $filters = json_decode((string) $row->filters, true); + if (!is_array($filters)) continue; + + // Extract product lists from filters: + // Example (your screenshot): + // {"relation":"or","176...":{"type":"products","method":"in_list","values":["1831"]}} + $filterProductLists = []; + + foreach ($filters as $k => $v) { + if ($k === 'relation') continue; + if (!is_array($v)) continue; + + $type = $v['type'] ?? ''; + $method = $v['method'] ?? ''; + $values = $v['values'] ?? []; + + if ($type !== 'products') continue; + if (!in_array($method, ['in_list', 'in', 'include', 'equals'], true)) continue; + if (!is_array($values)) continue; + + $ids = array_values(array_unique(array_map('intval', $values))); + $ids = array_values(array_filter($ids)); + + if (!empty($ids)) { + $filterProductLists[] = $ids; + } } - echo json_encode($enriched); - } catch (Exception $e) { echo json_encode([]); } - exit; + + if (empty($filterProductLists)) continue; + + // For each list: if a product is in that list, relate it to the other products in that list + foreach ($filterProductLists as $list) { + foreach ($list as $pid) { + $related = array_values(array_diff($list, [$pid])); + if (empty($related)) continue; + + // Keep only real Woo products + $related = array_values(array_filter($related, function($id){ + $pt = get_post_type($id); + return $pt === 'product' || $pt === 'product_variation'; + })); + + if (empty($related)) continue; + + if (!isset($map[$pid])) $map[$pid] = []; + $map[$pid] = array_values(array_unique(array_merge($map[$pid], $related))); + } + } + } + + return $map; } +function ss_cuw_build_deals_map() { + global $wpdb; + + $table = $wpdb->prefix . 'cuw_campaigns'; + + $exists = $wpdb->get_var( $wpdb->prepare("SHOW TABLES LIKE %s", $table) ); + if ($exists !== $table) return []; + + $rows = $wpdb->get_results(" + SELECT id, type, title, priority, filters, data + FROM {$table} + WHERE enabled = 1 + ORDER BY priority DESC, id DESC + "); + + if (empty($rows)) return []; + + $map = []; + + foreach ($rows as $row) { + $type = (string) $row->type; + if ($type !== 'buy_more_save_more') { + continue; + } + + $filters = json_decode((string) $row->filters, true); + $data = json_decode((string) $row->data, true); + + if (!is_array($filters) || !is_array($data)) continue; + + // 1) Product IDs uit filters halen (zoals jouw screenshot: type=products, method=in_list, values=[...]) + $product_ids = []; + + foreach ($filters as $k => $v) { + if ($k === 'relation') continue; + if (!is_array($v)) continue; + + $f_type = $v['type'] ?? ''; + $f_method = $v['method'] ?? ''; + $values = $v['values'] ?? []; + + if ($f_type !== 'products') continue; + if (!in_array($f_method, ['in_list', 'in', 'include', 'equals'], true)) continue; + if (!is_array($values)) continue; + + foreach ($values as $pid) { + $pid = (int) $pid; + if ($pid > 0) $product_ids[] = $pid; + } + } + + $product_ids = array_values(array_unique(array_filter($product_ids))); + if (empty($product_ids)) continue; + + // 2) Deal details uit data halen + $cta_text = $data['template']['cta_text'] ?? ''; + $display_location = $data['display_location'] ?? ($data['template']['display_location'] ?? ''); + + $discounts = $data['discounts'] ?? []; + if (!is_array($discounts) || empty($discounts)) continue; + + foreach ($discounts as $disc) { + if (!is_array($disc)) continue; + + $qty = isset($disc['qty']) ? (int) $disc['qty'] : 0; + $dType = isset($disc['type']) ? (string) $disc['type'] : ''; + $value = isset($disc['value']) ? (float) $disc['value'] : 0.0; + + if ($qty <= 0 || $dType === '' || $value <= 0) continue; + + $deal = [ + 'campaign_id' => (int) $row->id, + 'campaign_type' => $type, + 'title' => (string) $row->title, + 'priority' => (int) $row->priority, + 'qty' => $qty, + 'discount_type' => $dType, // fixed_price + 'value' => $value, // 10 (korting op 2e in jouw geval) + 'cta_text' => (string) $cta_text, + 'display_location' => (string) $display_location, + ]; + + foreach ($product_ids as $pid) { + if (!isset($map[$pid])) $map[$pid] = []; + $map[$pid][] = $deal; + } + } + } + + return $map; +} + + +// --- POSTCODE CHECK --- 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); + $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; + echo curl_exec($ch); + exit; } -// --- CREATE ORDER (V8.1 - MET LOGGING) --- +// --- GET PRODUCTS --- +// --- GET PRODUCTS --- +if ($action === 'get_products') { + try { + $products = $woocommerce->get('products', ['status' => 'publish', 'per_page' => 100]); + $cuw_map = ss_cuw_build_product_map(); + + $enriched = []; + + foreach ($products as $product) { + + // variations (bestaand) + $variation_details = ($product->type === 'variable') + ? (array) $woocommerce->get("products/{$product->id}/variations", ['per_page' => 50]) + : []; + + // wpupsell IDs uit meta_data halen (heuristisch, werkt bij veel plugins) + $wpupsell_ids = []; + if (!empty($product->meta_data) && is_array($product->meta_data)) { + foreach ($product->meta_data as $md) { + $key = isset($md->key) ? (string) $md->key : ''; + if ($key === '' || !preg_match('/wp\s*upsell|wpupsell|upsell\s*campaign/i', $key)) { + continue; + } + + $val = $md->value ?? null; + + // value kan array/object/string zijn + if (is_array($val)) { + $flat = json_encode($val); + if (preg_match_all('/\b\d+\b/', $flat, $m)) { + foreach ($m[0] as $id) $wpupsell_ids[] = (int) $id; + } + } elseif (is_object($val)) { + $flat = json_encode($val); + if (preg_match_all('/\b\d+\b/', $flat, $m)) { + foreach ($m[0] as $id) $wpupsell_ids[] = (int) $id; + } + } else { + $flat = (string) $val; + if (preg_match_all('/\b\d+\b/', $flat, $m)) { + foreach ($m[0] as $id) $wpupsell_ids[] = (int) $id; + } + } + } + } + + // upsell + cross-sell + wpupsell bundelen + $upsell_ids = !empty($product->upsell_ids) ? array_map('intval', (array) $product->upsell_ids) : []; + $cross_sell_ids = !empty($product->cross_sell_ids) ? array_map('intval', (array) $product->cross_sell_ids) : []; + + $cuw_ids = $cuw_map[(int)$product->id] ?? []; + + $recommended_ids = array_values(array_unique(array_filter(array_merge( + $upsell_ids, + $cross_sell_ids, + $cuw_ids + )))); + + // product naar array + velden toevoegen + $p = (array) $product; + $p['variation_details'] = $variation_details; + + // expliciet meesturen voor frontend + $p['cuw_ids'] = $cuw_ids; + $p['recommended_ids'] = $recommended_ids; + + $enriched[] = $p; + } + + echo json_encode($enriched); + } catch (Exception $e) { + echo json_encode([]); + } + exit; +} + + +// --- CREATE ORDER --- 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); + + $input['payment_method'] = 'cod'; + $input['payment_method_title'] = 'Sales Panel Order'; + $input['status'] = 'on-hold'; - // 1. ACCOUNT SYNC - $existing_customers = $woocommerce->get('customers', ['email' => $email]); - $input['customer_id'] = !empty($existing_customers) ? $existing_customers[0]->id : 0; + $existing = $woocommerce->get('customers', ['email' => $email]); + $input['customer_id'] = !empty($existing) ? $existing[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, '.', '')]]; + $shipping_incl = (float)($input['shipping_total'] ?? 0); + if ($shipping_incl > 0) { + $input['shipping_lines'] = [[ + 'method_id' => 'flat_rate', + 'method_title' => 'Verzendkosten', + 'total' => number_format($shipping_incl / 1.21, 4, '.', '') + ]]; } - $input['payment_method'] = $wc_gateway_id; $input['customer_note'] = "Agent: {$_SESSION['user']} | Mediacode: $mediacode"; - // 3. ATTRIBUTION METADATA + // --- ATTRIBUTION DATA (GEFIXTE SYNTAX) --- $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]; + $input['meta_data'][] = ['key' => 'Bron', 'value' => 'SalesPanel']; - // 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,-"); - } + $action_type = 'order_created'; + $log_stmt = $db->prepare("INSERT INTO sales_logs (username, action_type, order_id, amount, mediacode, customer_email, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW())"); + $log_stmt->bind_param("ssidss", $_SESSION['user'], $action_type, $order->id, $order->total, $mediacode, $email); + $log_stmt->execute(); - // 5. MOLLIE PAYMENT - $mollie = new MollieApiClient(); - $mollie->setApiKey($_ENV['MOLLIE_KEY']); - - $paymentData = [ - "amount" => ["currency" => "EUR", "value" => number_format((float)$order->total, 2, '.', '')], - "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=" . urlencode($mediacode), - "webhookUrl" => $_ENV['WC_URL'] . "/wc-api/{$wc_gateway_id}/?key={$order->order_key}&order_id={$order->id}", - "method" => $mollie_method, - "metadata" => ["order_id" => (string)$order->id, "mediacode" => $mediacode] - ]; - - if (in_array($mollie_method, ['in3', 'klarna', 'klarnapaylater', 'klarnasliceit', 'riverty'])) { - if ($mollie_method === 'riverty') $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, '.', '')]]]; - } - - $payment = $mollie->payments->create($paymentData); - - // 7. FINISH ORDER - $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:
" . $payment->getCheckoutUrl(), 'customer_note' => true]); - writeLog('ORDER_CREATED', "Order #{$order->id} voor {$input['billing']['email']}"); - - echo json_encode(['payment_url' => $payment->getCheckoutUrl()]); + echo json_encode(['success' => true, 'order_id' => $order->id, 'total' => $order->total]); } catch (Exception $e) { - writeLog('ERROR', $e->getMessage()); http_response_code(422); echo json_encode(['error' => $e->getMessage()]); } exit; diff --git a/index.html b/index.html index f10ee0e..89085ad 100644 --- a/index.html +++ b/index.html @@ -2,19 +2,23 @@ - Telvero Sales Panel + Telvero Sales Panel V9 - - -