Discountrules added

This commit is contained in:
Mark Pinkster 2026-01-10 17:26:46 +01:00
parent 79d4861e6d
commit cadd9d962e
11 changed files with 1436 additions and 30 deletions

2
.gitignore vendored
View File

@ -8,7 +8,7 @@
# Local History for Visual Studio Code # Local History for Visual Studio Code
.history/ .history/
woo-discount-rules/*
# Built Visual Studio Code Extensions # Built Visual Studio Code Extensions
*.vsix *.vsix
.env .env

View File

@ -3,9 +3,11 @@
* Products Action - Get products with enriched data * Products Action - Get products with enriched data
* *
* Optimized to fetch only required fields for telesales app * Optimized to fetch only required fields for telesales app
* Includes BOGO rules from Woo Discount Rules plugin
*/ */
require_once __DIR__ . '/../services/UpsellService.php'; require_once __DIR__ . '/../services/UpsellService.php';
require_once __DIR__ . '/../services/DiscountRulesService.php';
/** /**
* Handle get_products action * Handle get_products action
@ -14,8 +16,27 @@ require_once __DIR__ . '/../services/UpsellService.php';
*/ */
function handleGetProducts(): void function handleGetProducts(): void
{ {
$logFile = __DIR__ . '/../../logs/products_debug.log';
$logDir = dirname($logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$log = function($msg, $data = null) use ($logFile) {
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] {$msg}";
if ($data !== null) {
$logMessage .= "\n" . print_r($data, true);
}
$logMessage .= "\n" . str_repeat('-', 80) . "\n";
file_put_contents($logFile, $logMessage, FILE_APPEND);
};
try { try {
$log("handleGetProducts() started");
$woocommerce = getWooCommerce(); $woocommerce = getWooCommerce();
$log("WooCommerce client initialized");
// Only fetch fields needed for telesales app // Only fetch fields needed for telesales app
$productFields = 'id,name,price,type,upsell_ids,cross_sell_ids'; $productFields = 'id,name,price,type,upsell_ids,cross_sell_ids';
@ -26,12 +47,29 @@ function handleGetProducts(): void
'per_page' => 100, 'per_page' => 100,
'_fields' => $productFields '_fields' => $productFields
]); ]);
$log("Fetched products from WooCommerce", ['count' => count($products)]);
// Build service maps
$cuw_map = UpsellService::buildProductMap(); $cuw_map = UpsellService::buildProductMap();
$log("Built CUW map");
// Get BOGO rules from Woo Discount Rules
$log("Creating DiscountRulesService...");
$discountService = new DiscountRulesService();
$log("DiscountRulesService created");
$productIds = array_map(fn($p) => (int) $p->id, $products);
$log("Product IDs extracted", ['count' => count($productIds)]);
$log("Calling buildProductBogoMap...");
$bogo_map = $discountService->buildProductBogoMap($productIds);
$log("BOGO map built", ['products_with_bogo' => count($bogo_map)]);
$enriched = []; $enriched = [];
foreach ($products as $product) { foreach ($products as $product) {
$productId = (int) $product->id;
// Get variations for variable products (only needed fields) // Get variations for variable products (only needed fields)
$variation_details = []; $variation_details = [];
if ($product->type === 'variable') { if ($product->type === 'variable') {
@ -52,17 +90,20 @@ function handleGetProducts(): void
? array_map('intval', (array) $product->cross_sell_ids) ? array_map('intval', (array) $product->cross_sell_ids)
: []; : [];
$cuw_ids = $cuw_map[(int)$product->id] ?? []; $cuw_ids = $cuw_map[$productId] ?? [];
$recommended_ids = array_values(array_unique(array_filter(array_merge( $recommended_ids = array_values(array_unique(array_filter(array_merge(
$upsell_ids, $upsell_ids,
$cross_sell_ids, $cross_sell_ids,
$cuw_ids $cuw_ids
)))); ))));
// Get BOGO rules for this product
$bogo_rules = $bogo_map[$productId] ?? [];
// Build minimal product object with only needed fields // Build minimal product object with only needed fields
$enriched[] = [ $enriched[] = [
'id' => (int) $product->id, 'id' => $productId,
'name' => $product->name, 'name' => $product->name,
'price' => $product->price, 'price' => $product->price,
'type' => $product->type, 'type' => $product->type,
@ -70,12 +111,28 @@ function handleGetProducts(): void
'cross_sell_ids' => $cross_sell_ids, 'cross_sell_ids' => $cross_sell_ids,
'variation_details' => $variation_details, 'variation_details' => $variation_details,
'cuw_ids' => $cuw_ids, 'cuw_ids' => $cuw_ids,
'recommended_ids' => $recommended_ids 'recommended_ids' => $recommended_ids,
'bogo_rules' => $bogo_rules
]; ];
} }
$log("Returning " . count($enriched) . " enriched products");
echo json_encode($enriched); echo json_encode($enriched);
} catch (Exception $e) { } catch (Exception $e) {
echo json_encode([]); $log("ERROR in handleGetProducts", [
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
]);
echo json_encode(['error' => $e->getMessage()]);
} catch (Error $e) {
$log("FATAL ERROR in handleGetProducts", [
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString()
]);
echo json_encode(['error' => $e->getMessage()]);
} }
} }

View File

@ -6,7 +6,7 @@
use Automattic\WooCommerce\Client; use Automattic\WooCommerce\Client;
/** /**
* Get database connection * Get database connection (Sales Panel database)
* @return mysqli * @return mysqli
*/ */
function getDatabase(): mysqli function getDatabase(): mysqli
@ -33,6 +33,44 @@ function getDatabase(): mysqli
return $db; return $db;
} }
/**
* Get WooCommerce database connection (WordPress/WooCommerce database)
* Used for accessing plugin tables like wp_wdr_rules
* @return mysqli
*/
function getWooCommerceDatabase(): mysqli
{
static $wcDb = null;
if ($wcDb === null) {
$wcDb = new mysqli(
$_ENV['WC_DB_HOST'] ?? $_ENV['DB_HOST'],
$_ENV['WC_DB_USER'] ?? $_ENV['DB_USER'],
$_ENV['WC_DB_PASS'] ?? $_ENV['DB_PASS'],
$_ENV['WC_DB_NAME'] ?? $_ENV['DB_NAME']
);
if ($wcDb->connect_error) {
header('Content-Type: application/json');
http_response_code(500);
die(json_encode(['error' => 'WooCommerce database connectie mislukt: ' . $wcDb->connect_error]));
}
$wcDb->set_charset('utf8mb4');
}
return $wcDb;
}
/**
* Get WooCommerce database table prefix
* @return string
*/
function getWooCommerceDbPrefix(): string
{
return $_ENV['WC_DB_PREFIX'] ?? 'wp_';
}
/** /**
* Get WooCommerce client * Get WooCommerce client
* @return Client * @return Client

View File

@ -0,0 +1,542 @@
<?php
/**
* DiscountRulesService - Fetches BOGO rules from Woo Discount Rules plugin
*
* Queries the wp_wdr_rules table to get active Buy X Get X and Buy X Get Y rules
* and maps them to products for the telesales panel.
*/
class DiscountRulesService
{
private mysqli $db;
private string $tablePrefix;
private bool $debug = true;
private string $logFile;
public function __construct()
{
// Use WooCommerce database for accessing plugin tables
$this->db = getWooCommerceDatabase();
$this->tablePrefix = getWooCommerceDbPrefix();
$this->logFile = __DIR__ . '/../../logs/bogo_debug.log';
// Ensure logs directory exists
$logDir = dirname($this->logFile);
if (!is_dir($logDir)) {
mkdir($logDir, 0755, true);
}
$this->log("DiscountRulesService initialized", [
'tablePrefix' => $this->tablePrefix,
'db_connected' => $this->db->ping()
]);
}
/**
* Log debug message
*
* @param string $message
* @param mixed $data
*/
private function log(string $message, $data = null): void
{
if (!$this->debug) return;
$timestamp = date('Y-m-d H:i:s');
$logMessage = "[{$timestamp}] {$message}";
if ($data !== null) {
$logMessage .= "\n" . print_r($data, true);
}
$logMessage .= "\n" . str_repeat('-', 80) . "\n";
file_put_contents($this->logFile, $logMessage, FILE_APPEND);
}
/**
* Get all active BOGO rules mapped by product ID
*
* @return array<int, array> Map of product_id => array of BOGO rules
*/
public function getBogoRulesMap(): array
{
$rules = $this->fetchActiveBogoRules();
return $this->mapRulesToProducts($rules);
}
/**
* Fetch active BOGO rules from database
*
* @return array
*/
private function fetchActiveBogoRules(): array
{
$table = $this->tablePrefix . 'wdr_rules';
$currentTime = time();
$this->log("fetchActiveBogoRules() called", [
'table' => $table,
'currentTime' => $currentTime,
'tablePrefix' => $this->tablePrefix
]);
// First check if table exists
$tableCheck = $this->db->query("SHOW TABLES LIKE '{$table}'");
if ($tableCheck->num_rows === 0) {
$this->log("ERROR: Table {$table} does not exist!");
return [];
}
$this->log("Table {$table} exists");
$query = "SELECT
id,
title,
filters,
buy_x_get_x_adjustments,
buy_x_get_y_adjustments,
advanced_discount_message
FROM {$table}
WHERE enabled = 1
AND deleted = 0
AND (date_from <= ? OR date_from IS NULL)
AND (date_to >= ? OR date_to IS NULL)
AND (usage_limits > used_limits OR used_limits IS NULL OR usage_limits = 0)
AND (
(buy_x_get_x_adjustments IS NOT NULL AND buy_x_get_x_adjustments != '{}' AND buy_x_get_x_adjustments != '[]')
OR (buy_x_get_y_adjustments IS NOT NULL AND buy_x_get_y_adjustments != '{}' AND buy_x_get_y_adjustments != '[]')
)
ORDER BY priority ASC";
$this->log("Executing query", $query);
$stmt = $this->db->prepare($query);
if (!$stmt) {
$this->log("ERROR: Failed to prepare statement", $this->db->error);
return [];
}
$stmt->bind_param('ii', $currentTime, $currentTime);
if (!$stmt->execute()) {
$this->log("ERROR: Failed to execute statement", $stmt->error);
$stmt->close();
return [];
}
$result = $stmt->get_result();
$rules = [];
while ($row = $result->fetch_assoc()) {
$rules[] = $row;
}
$this->log("Found " . count($rules) . " BOGO rules", $rules);
$stmt->close();
return $rules;
}
/**
* Map rules to products based on filters
*
* @param array $rules
* @return array<int, array>
*/
private function mapRulesToProducts(array $rules): array
{
$productMap = [];
foreach ($rules as $rule) {
$productIds = $this->extractProductIdsFromFilters($rule['filters']);
$bogoData = $this->parseBogoAdjustments($rule);
if (empty($bogoData)) {
continue;
}
foreach ($productIds as $productId) {
if (!isset($productMap[$productId])) {
$productMap[$productId] = [];
}
$productMap[$productId][] = $bogoData;
}
}
return $productMap;
}
/**
* Extract product IDs from rule filters
*
* @param string|null $filtersJson
* @return array<int>
*/
private function extractProductIdsFromFilters(?string $filtersJson): array
{
if (empty($filtersJson)) {
return [];
}
$filters = json_decode($filtersJson, true);
if (!is_array($filters)) {
return [];
}
$productIds = [];
foreach ($filters as $filter) {
$type = $filter['type'] ?? '';
$value = $filter['value'] ?? [];
// Direct product filter
if ($type === 'products' && !empty($value)) {
$productIds = array_merge($productIds, array_map('intval', $value));
}
// Category filter - we'd need to expand this to actual products
// For now, we'll handle this separately if needed
if ($type === 'product_category' && !empty($value)) {
$categoryProducts = $this->getProductsByCategories($value);
$productIds = array_merge($productIds, $categoryProducts);
}
// All products filter
if ($type === 'all_products') {
// Return empty to indicate "all products" - handled differently
return ['all'];
}
}
return array_unique($productIds);
}
/**
* Get product IDs by category IDs
*
* @param array $categoryIds
* @return array<int>
*/
private function getProductsByCategories(array $categoryIds): array
{
if (empty($categoryIds)) {
return [];
}
$table = $this->tablePrefix . 'term_relationships';
$placeholders = implode(',', array_fill(0, count($categoryIds), '?'));
$query = "SELECT DISTINCT object_id
FROM {$table}
WHERE term_taxonomy_id IN ({$placeholders})";
$stmt = $this->db->prepare($query);
$types = str_repeat('i', count($categoryIds));
$stmt->bind_param($types, ...$categoryIds);
$stmt->execute();
$result = $stmt->get_result();
$productIds = [];
while ($row = $result->fetch_assoc()) {
$productIds[] = (int) $row['object_id'];
}
$stmt->close();
return $productIds;
}
/**
* Parse BOGO adjustments from rule
*
* @param array $rule
* @return array|null
*/
private function parseBogoAdjustments(array $rule): ?array
{
// Try Buy X Get X first
$bxgx = $this->parseBuyXGetX($rule);
if ($bxgx) {
return $bxgx;
}
// Try Buy X Get Y
$bxgy = $this->parseBuyXGetY($rule);
if ($bxgy) {
return $bxgy;
}
return null;
}
/**
* Extract badge_text from advanced_discount_message
*
* @param array $rule
* @return string|null
*/
private function extractBadgeText(array $rule): ?string
{
$advancedMessage = $rule['advanced_discount_message'] ?? null;
if (empty($advancedMessage)) {
return null;
}
$data = json_decode($advancedMessage, true);
if (!is_array($data)) {
return null;
}
// Look for badge_text in discount_badge
if (isset($data['discount_badge']['badge_text']) && !empty($data['discount_badge']['badge_text'])) {
return $data['discount_badge']['badge_text'];
}
// Fallback to badge_text at root level
if (isset($data['badge_text']) && !empty($data['badge_text'])) {
return $data['badge_text'];
}
return null;
}
/**
* Parse Buy X Get X adjustments (e.g., 1+1 free)
*
* @param array $rule
* @return array|null
*/
private function parseBuyXGetX(array $rule): ?array
{
$json = $rule['buy_x_get_x_adjustments'] ?? null;
if (empty($json) || $json === '{}' || $json === '[]') {
return null;
}
$data = json_decode($json, true);
if (!isset($data['ranges']) || empty($data['ranges'])) {
return null;
}
// Get the first range (most common case)
$range = reset($data['ranges']);
if (!$range) {
return null;
}
$buyQty = (int) ($range['from'] ?? 0);
$getQty = (int) ($range['free_qty'] ?? 0);
$freeType = $range['free_type'] ?? 'free_product';
$freeValue = (float) ($range['free_value'] ?? 100);
$recursive = !empty($range['recursive']);
if ($buyQty <= 0 || $getQty <= 0) {
return null;
}
// Generate human-readable label (fallback)
$label = $this->generateBogoLabel($buyQty, $getQty, $freeType, $freeValue);
// Extract badge_text from advanced_discount_message
$badgeText = $this->extractBadgeText($rule);
return [
'rule_id' => (int) $rule['id'],
'rule_title' => $rule['title'],
'type' => 'buy_x_get_x',
'label' => $label,
'badge_text' => $badgeText,
'buy_qty' => $buyQty,
'get_qty' => $getQty,
'discount_type' => $freeType,
'discount_value' => $freeValue,
'recursive' => $recursive,
'get_product_ids' => null // Same product
];
}
/**
* Parse Buy X Get Y adjustments (e.g., buy A get B free)
*
* @param array $rule
* @return array|null
*/
private function parseBuyXGetY(array $rule): ?array
{
$json = $rule['buy_x_get_y_adjustments'] ?? null;
if (empty($json) || $json === '{}' || $json === '[]') {
return null;
}
$data = json_decode($json, true);
// Check if it's a valid BXGY rule
$type = $data['type'] ?? '';
if ($type !== 'bxgy_product' && $type !== 'bxgy_category') {
return null;
}
if (!isset($data['ranges']) || empty($data['ranges'])) {
return null;
}
// Get the first range
$range = reset($data['ranges']);
if (!$range) {
return null;
}
$buyQty = (int) ($range['from'] ?? 0);
$getQty = (int) ($range['free_qty'] ?? 0);
$freeType = $range['free_type'] ?? 'free_product';
$freeValue = (float) ($range['free_value'] ?? 100);
$getProductIds = $range['products'] ?? [];
$recursive = !empty($range['recursive']);
if ($buyQty <= 0 || $getQty <= 0) {
return null;
}
// Generate human-readable label (fallback)
$label = $this->generateBogoLabel($buyQty, $getQty, $freeType, $freeValue, true);
// Extract badge_text from advanced_discount_message
$badgeText = $this->extractBadgeText($rule);
return [
'rule_id' => (int) $rule['id'],
'rule_title' => $rule['title'],
'type' => 'buy_x_get_y',
'label' => $label,
'badge_text' => $badgeText,
'buy_qty' => $buyQty,
'get_qty' => $getQty,
'discount_type' => $freeType,
'discount_value' => $freeValue,
'recursive' => $recursive,
'get_product_ids' => array_map('intval', $getProductIds)
];
}
/**
* Generate human-readable BOGO label
*
* @param int $buyQty
* @param int $getQty
* @param string $discountType
* @param float $discountValue
* @param bool $isDifferentProduct
* @return string
*/
private function generateBogoLabel(
int $buyQty,
int $getQty,
string $discountType,
float $discountValue,
bool $isDifferentProduct = false
): string {
// Common patterns
if ($discountType === 'free_product' || $discountValue >= 100) {
if ($buyQty === 1 && $getQty === 1) {
return $isDifferentProduct ? 'Koop 1, krijg 1 ander gratis' : '1+1 Gratis';
}
if ($buyQty === 2 && $getQty === 1) {
return $isDifferentProduct ? 'Koop 2, krijg 1 ander gratis' : '2+1 Gratis';
}
if ($buyQty === 3 && $getQty === 1) {
return $isDifferentProduct ? 'Koop 3, krijg 1 ander gratis' : '3+1 Gratis';
}
return "Koop {$buyQty}, krijg {$getQty} gratis";
}
if ($discountType === 'percentage') {
$pct = (int) $discountValue;
return "Koop {$buyQty}, krijg {$getQty} met {$pct}% korting";
}
if ($discountType === 'flat') {
return "Koop {$buyQty}, krijg {$getQty} met €" . number_format($discountValue, 2, ',', '.') . " korting";
}
return "Koop {$buyQty}, krijg {$getQty}";
}
/**
* Build a complete product map including "all products" rules
*
* @param array $productIds List of all product IDs to check
* @return array<int, array>
*/
public function buildProductBogoMap(array $productIds): array
{
$this->log("buildProductBogoMap() called", [
'productIds_count' => count($productIds),
'productIds_sample' => array_slice($productIds, 0, 5)
]);
try {
$rules = $this->fetchActiveBogoRules();
$this->log("Fetched rules", ['count' => count($rules)]);
$productMap = [];
$allProductsRules = [];
foreach ($rules as $rule) {
$this->log("Processing rule", [
'id' => $rule['id'],
'title' => $rule['title'],
'filters' => $rule['filters']
]);
$filterProductIds = $this->extractProductIdsFromFilters($rule['filters']);
$this->log("Extracted filter product IDs", $filterProductIds);
$bogoData = $this->parseBogoAdjustments($rule);
$this->log("Parsed BOGO data", $bogoData);
if (empty($bogoData)) {
$this->log("Skipping rule - no BOGO data");
continue;
}
// Check if this is an "all products" rule
if ($filterProductIds === ['all']) {
$this->log("Rule applies to all products");
$allProductsRules[] = $bogoData;
continue;
}
foreach ($filterProductIds as $productId) {
if (!isset($productMap[$productId])) {
$productMap[$productId] = [];
}
$productMap[$productId][] = $bogoData;
}
}
// Apply "all products" rules to all products
if (!empty($allProductsRules)) {
$this->log("Applying 'all products' rules", ['count' => count($allProductsRules)]);
foreach ($productIds as $productId) {
if (!isset($productMap[$productId])) {
$productMap[$productId] = [];
}
$productMap[$productId] = array_merge($productMap[$productId], $allProductsRules);
}
}
$this->log("Final product map", [
'products_with_bogo' => count($productMap),
'sample' => array_slice($productMap, 0, 3, true)
]);
return $productMap;
} catch (Exception $e) {
$this->log("ERROR in buildProductBogoMap", [
'message' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [];
}
}
}

View File

@ -22,6 +22,7 @@
</style> </style>
<!-- Load JavaScript modules --> <!-- Load JavaScript modules -->
<script src="js/services/api.js"></script> <script src="js/services/api.js"></script>
<script src="js/services/bogo.js"></script>
<script src="js/components/cart.js"></script> <script src="js/components/cart.js"></script>
<script src="js/components/products.js"></script> <script src="js/components/products.js"></script>
<script src="js/components/forms.js"></script> <script src="js/components/forms.js"></script>
@ -136,7 +137,13 @@
<div @click="selectedProductId = p.id; selectProduct(); open = false; productSearch = ''" <div @click="selectedProductId = p.id; selectProduct(); open = false; productSearch = ''"
class="p-4 hover:bg-blue-50 cursor-pointer font-bold text-sm text-slate-700" class="p-4 hover:bg-blue-50 cursor-pointer font-bold text-sm text-slate-700"
:class="{ 'bg-blue-100': selectedProductId == p.id }"> :class="{ 'bg-blue-100': selectedProductId == p.id }">
<span x-text="p.name"></span> <div class="flex items-center justify-between">
<span x-text="p.name"></span>
<span x-show="p.bogo_rules && p.bogo_rules.length > 0"
x-text="p.bogo_rules[0]?.badge_text || p.bogo_rules[0]?.label"
class="ml-2 px-2 py-1 bg-red-500 text-white text-[9px] font-black rounded-full uppercase">
</span>
</div>
</div> </div>
</template> </template>
<div x-show="filteredProducts.length === 0" class="p-4 text-slate-400 text-sm text-center"> <div x-show="filteredProducts.length === 0" class="p-4 text-slate-400 text-sm text-center">
@ -146,6 +153,32 @@
</div> </div>
</div> </div>
<!-- BOGO Actie Banner -->
<div x-show="activeProduct && activeProduct.bogo_rules && activeProduct.bogo_rules.length > 0" x-cloak
class="mb-6 p-4 bg-gradient-to-r from-red-500 to-pink-500 rounded-2xl text-white shadow-lg">
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<div class="bg-white/20 p-2 rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7" />
</svg>
</div>
<div>
<p class="font-black text-sm uppercase tracking-wide" x-text="activeProduct?.bogo_rules?.[0]?.badge_text || activeProduct?.bogo_rules?.[0]?.label || ''"></p>
<p class="text-[10px] opacity-80" x-text="'Actie: ' + (activeProduct?.bogo_rules?.[0]?.rule_title || '')"></p>
</div>
</div>
<!-- Button for flat/percentage discount BOGO rules -->
<button
x-show="activeProduct?.bogo_rules?.[0]?.discount_type === 'flat' || activeProduct?.bogo_rules?.[0]?.discount_type === 'percentage'"
@click="addBogoDiscountedItem(activeProduct)"
:disabled="!canAddBogoDiscountedItem(activeProduct)"
class="bg-white text-red-500 px-4 py-2 rounded-xl font-black text-xs uppercase shadow-md hover:bg-red-50 transition disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap">
<span x-text="'+ €' + getBogoDiscountedPrice(activeProduct)"></span>
</button>
</div>
</div>
<div x-show="variations.length > 0" x-cloak <div x-show="variations.length > 0" x-cloak
class="mb-8 p-6 bg-blue-50 rounded-3xl border border-blue-100 shadow-inner"> class="mb-8 p-6 bg-blue-50 rounded-3xl border border-blue-100 shadow-inner">
<select x-model="selectedVariationId" @change="selectVariation()" <select x-model="selectedVariationId" @change="selectVariation()"
@ -269,14 +302,33 @@
<div <div
class="space-y-4 mb-6 min-h-[100px] max-h-[300px] overflow-y-auto pr-2 custom-scrollbar"> class="space-y-4 mb-6 min-h-[100px] max-h-[300px] 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"
:class="{
'bg-green-900/30 -mx-2 px-2 py-1 rounded-lg': item.isFree,
'bg-amber-900/30 -mx-2 px-2 py-1 rounded-lg': item.isDiscounted && !item.isFree
}">
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<span x-text="item.name" <span x-text="item.name"
class="text-[11px] font-medium leading-tight text-slate-300"></span> class="text-[11px] font-medium leading-tight"
<span x-text="'€' + item.price" :class="{
class="text-[11px] font-black text-blue-400"></span> 'text-green-400': item.isFree,
'text-amber-400': item.isDiscounted && !item.isFree,
'text-slate-300': !item.isFree && !item.isDiscounted
}"></span>
<div class="flex items-center gap-2">
<span x-show="(item.isFree || item.isDiscounted) && item.originalPrice"
x-text="'€' + item.originalPrice"
class="text-[10px] text-slate-500 line-through"></span>
<span x-text="'€' + item.price"
class="text-[11px] font-black"
:class="{
'text-green-400': item.isFree,
'text-amber-400': item.isDiscounted && !item.isFree,
'text-blue-400': !item.isFree && !item.isDiscounted
}"></span>
</div>
</div> </div>
<button @click="removeFromCart(index)" <button x-show="!item.isFree" @click="removeFromCart(index)"
class="text-slate-600 hover:text-red-500 transition"> class="text-slate-600 hover:text-red-500 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none"
viewBox="0 0 24 24" stroke="currentColor"> viewBox="0 0 24 24" stroke="currentColor">
@ -284,6 +336,12 @@
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" /> 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>
</button> </button>
<span x-show="item.isFree && !item.isDiscounted" class="text-green-400 text-[9px] font-black uppercase">
Gratis
</span>
<span x-show="item.isDiscounted && !item.isFree" class="text-amber-400 text-[9px] font-black uppercase">
Korting
</span>
</div> </div>
</template> </template>
</div> </div>

View File

@ -27,6 +27,14 @@ function salesApp() {
return CartComponent.getComputed().total.call(this); return CartComponent.getComputed().total.call(this);
}, },
get subtotal() {
return CartComponent.getComputed().subtotal.call(this);
},
get freeItemsCount() {
return CartComponent.getComputed().freeItemsCount.call(this);
},
get filteredProducts() { get filteredProducts() {
return ProductsComponent.getComputed().filteredProducts.call(this); return ProductsComponent.getComputed().filteredProducts.call(this);
}, },

View File

@ -1,5 +1,5 @@
/** /**
* Cart Component - Handles shopping cart functionality * Cart Component - Handles shopping cart functionality with BOGO support
*/ */
const CartComponent = { const CartComponent = {
/** /**
@ -9,7 +9,10 @@ const CartComponent = {
getInitialState() { getInitialState() {
return { return {
cart: [], cart: [],
shipping: '8.95' shipping: '8.95',
bogoDiscount: 0,
bogoFreeItems: [],
appliedBogoRules: []
}; };
}, },
@ -20,7 +23,7 @@ const CartComponent = {
getMethods() { getMethods() {
return { return {
/** /**
* Add item to cart * Add item to cart and recalculate BOGO
* @param {Object} item - Item with id, name, price, and optional variation_id * @param {Object} item - Item with id, name, price, and optional variation_id
*/ */
addToCart(item) { addToCart(item) {
@ -28,42 +31,213 @@ const CartComponent = {
id: parseInt(item.id), id: parseInt(item.id),
variation_id: item.variation_id || 0, variation_id: item.variation_id || 0,
name: item.name, name: item.name,
price: item.price price: item.price,
isFree: item.isFree || false
}); });
this.recalculateBogo();
}, },
/** /**
* Remove item from cart by index * Remove item from cart by index and recalculate BOGO
* @param {number} index * @param {number} index
*/ */
removeFromCart(index) { removeFromCart(index) {
const item = this.cart[index];
this.cart.splice(index, 1); this.cart.splice(index, 1);
// If removing a paid item, recalculate BOGO
if (!item.isFree) {
this.recalculateBogo();
}
}, },
/** /**
* Check if product is in cart * Check if product is in cart (excluding free items)
* @param {number} id * @param {number} id
* @returns {boolean} * @returns {boolean}
*/ */
isInCart(id) { isInCart(id) {
return this.cart.some(i => parseInt(i.id) === parseInt(id)); return this.cart.some(i => parseInt(i.id) === parseInt(id) && !i.isFree);
}, },
/** /**
* Toggle upsell product in cart * Toggle upsell product in cart
* @param {Object} product * @param {Object} product
*/ */
toggleUpsell(product) { toggleUpsell(product) {
const idx = this.cart.findIndex(i => parseInt(i.id) === parseInt(product.id)); const idx = this.cart.findIndex(i => parseInt(i.id) === parseInt(product.id) && !i.isFree);
if (idx > -1) { if (idx > -1) {
this.cart.splice(idx, 1); this.cart.splice(idx, 1);
} else { } else {
this.cart.push({ this.cart.push({
id: parseInt(product.id), id: parseInt(product.id),
name: product.name, name: product.name,
price: product.price price: product.price,
isFree: false
}); });
} }
this.recalculateBogo();
},
/**
* Recalculate BOGO discounts based on current cart
*/
recalculateBogo() {
// Remove existing free items from cart
this.cart = this.cart.filter(item => !item.isFree);
// Calculate new BOGO
const bogoResult = BogoService.calculateBogoForCart(this.cart, this.products);
this.bogoDiscount = bogoResult.discountAmount;
this.bogoFreeItems = bogoResult.freeItems;
this.appliedBogoRules = bogoResult.appliedRules;
// Add free items to cart
for (const freeItem of bogoResult.freeItems) {
this.cart.push({
id: parseInt(freeItem.id),
variation_id: 0,
name: freeItem.name,
price: freeItem.price,
originalPrice: freeItem.originalPrice,
isFree: true,
ruleId: freeItem.ruleId
});
}
},
/**
* Add discounted BOGO item to cart manually
* For rules like "2e voor €19,95" where the 2nd item gets a discount
* @param {Object} product - The product with BOGO rules
*/
addBogoDiscountedItem(product) {
if (!product || !product.bogo_rules || product.bogo_rules.length === 0) {
return;
}
const rule = product.bogo_rules[0];
// Check if this is a discount type rule (not free)
if (rule.discount_type !== 'flat' && rule.discount_type !== 'percentage') {
return;
}
// Calculate discounted price
let discountedPrice;
let discountLabel;
if (rule.discount_type === 'flat') {
discountedPrice = (parseFloat(product.price) - rule.discount_value).toFixed(2);
discountLabel = '€' + rule.discount_value.toFixed(2) + ' korting';
} else if (rule.discount_type === 'percentage') {
const discount = parseFloat(product.price) * (rule.discount_value / 100);
discountedPrice = (parseFloat(product.price) - discount).toFixed(2);
discountLabel = rule.discount_value + '% korting';
}
// Add the discounted item to cart (this IS the 2nd item, not a 3rd)
// isFree: false because it's not free, just discounted
this.cart.push({
id: parseInt(product.id),
variation_id: 0,
name: product.name + ' (' + discountLabel + ')',
price: discountedPrice,
originalPrice: product.price,
isFree: false,
isDiscounted: true,
ruleId: rule.rule_id
});
},
/**
* Check if BOGO discounted item can be added
* For "2e voor €19,95" type rules: user needs 1 item in cart, then can add the 2nd at discount
* @param {Object} product
* @returns {boolean}
*/
canAddBogoDiscountedItem(product) {
if (!product || !product.bogo_rules || product.bogo_rules.length === 0) {
return false;
}
const rule = product.bogo_rules[0];
// Only for flat or percentage discount types
if (rule.discount_type !== 'flat' && rule.discount_type !== 'percentage') {
return false;
}
// Count how many of this product are in cart (non-discounted items)
const fullPriceCount = this.cart.filter(item =>
parseInt(item.id) === parseInt(product.id) && !item.isDiscounted
).length;
// Count how many discounted items are already in cart
const discountedCount = this.cart.filter(item =>
parseInt(item.id) === parseInt(product.id) && item.isDiscounted
).length;
// For "buy 2 get 1 discount" rules:
// - buy_qty = 2 means you need to buy 2 total (1 full price + 1 discounted)
// - So user needs (buy_qty - get_qty) = 1 full price item to qualify for 1 discounted
const fullPriceNeeded = rule.buy_qty - rule.get_qty;
// Check if we have enough full-price items
if (fullPriceCount < fullPriceNeeded) {
return false;
}
// Calculate max allowed discounted items based on full-price items
let maxDiscounted = rule.get_qty;
if (rule.recursive) {
// For each set of (buy_qty - get_qty) full-price items, allow get_qty discounted
maxDiscounted = Math.floor(fullPriceCount / fullPriceNeeded) * rule.get_qty;
}
// Can add if we haven't reached the max
return discountedCount < maxDiscounted;
},
/**
* Get the discounted price for BOGO item
* @param {Object} product
* @returns {string|null}
*/
getBogoDiscountedPrice(product) {
if (!product || !product.bogo_rules || product.bogo_rules.length === 0) {
return null;
}
const rule = product.bogo_rules[0];
if (rule.discount_type === 'flat') {
return (parseFloat(product.price) - rule.discount_value).toFixed(2);
} else if (rule.discount_type === 'percentage') {
const discount = parseFloat(product.price) * (rule.discount_value / 100);
return (parseFloat(product.price) - discount).toFixed(2);
}
return null;
},
/**
* Get BOGO label for a product
* @param {Object} product
* @returns {string|null}
*/
getBogoLabel(product) {
return BogoService.getBogoLabel(product);
},
/**
* Check if product has BOGO rules
* @param {Object} product
* @returns {boolean}
*/
hasBogoRules(product) {
return BogoService.hasBogoRules(product);
}, },
/** /**
@ -71,6 +245,9 @@ const CartComponent = {
*/ */
clearCart() { clearCart() {
this.cart = []; this.cart = [];
this.bogoDiscount = 0;
this.bogoFreeItems = [];
this.appliedBogoRules = [];
}, },
/** /**
@ -79,6 +256,9 @@ const CartComponent = {
resetCart() { resetCart() {
this.cart = []; this.cart = [];
this.shipping = '8.95'; this.shipping = '8.95';
this.bogoDiscount = 0;
this.bogoFreeItems = [];
this.appliedBogoRules = [];
} }
}; };
}, },
@ -90,12 +270,36 @@ const CartComponent = {
getComputed() { getComputed() {
return { return {
/** /**
* Calculate total including shipping * Calculate subtotal (items only, no shipping, no BOGO discount)
* @returns {string}
*/
subtotal() {
return this.cart
.filter(item => !item.isFree)
.reduce((sum, item) => sum + parseFloat(item.price), 0)
.toFixed(2);
},
/**
* Calculate total including shipping and BOGO discount
* @returns {string} * @returns {string}
*/ */
total() { total() {
const itemsTotal = this.cart.reduce((sum, item) => sum + parseFloat(item.price), 0); const itemsTotal = this.cart
return (itemsTotal + (parseFloat(this.shipping) || 0)).toFixed(2); .filter(item => !item.isFree)
.reduce((sum, item) => sum + parseFloat(item.price), 0);
const shipping = parseFloat(this.shipping) || 0;
// Note: BOGO discount is already applied via free items with price 0
// So we don't subtract bogoDiscount here
return (itemsTotal + shipping).toFixed(2);
},
/**
* Get count of free items in cart
* @returns {number}
*/
freeItemsCount() {
return this.cart.filter(item => item.isFree).length;
} }
}; };
} }

View File

@ -49,14 +49,19 @@ const ProductsComponent = {
this.activeProduct = p; this.activeProduct = p;
this.variations = p.variation_details || []; this.variations = p.variation_details || [];
this.cart = []; this.cart = [];
this.bogoDiscount = 0;
this.bogoFreeItems = [];
this.appliedBogoRules = [];
this.selectedVariationId = ''; this.selectedVariationId = '';
if (p.type !== 'variable') { if (p.type !== 'variable') {
this.cart.push({ this.cart.push({
id: parseInt(p.id), id: parseInt(p.id),
name: p.name, name: p.name,
price: p.price price: p.price,
isFree: false
}); });
this.recalculateBogo();
this.loadRecommendations(p); this.loadRecommendations(p);
} }
}, },
@ -72,9 +77,11 @@ const ProductsComponent = {
id: parseInt(this.activeProduct.id), id: parseInt(this.activeProduct.id),
variation_id: parseInt(v.id), variation_id: parseInt(v.id),
name: this.activeProduct.name + ' - ' + this.getVarName(v), name: this.activeProduct.name + ' - ' + this.getVarName(v),
price: v.price price: v.price,
isFree: false
}]; }];
this.recalculateBogo();
this.loadRecommendations(this.activeProduct); this.loadRecommendations(this.activeProduct);
}, },
@ -88,8 +95,10 @@ const ProductsComponent = {
this.cart.push({ this.cart.push({
id: parseInt(p.id), id: parseInt(p.id),
name: p.name, name: p.name,
price: p.price price: p.price,
isFree: false
}); });
this.recalculateBogo();
this.extraProductId = ''; this.extraProductId = '';
}, },

189
js/services/bogo.js Normal file
View File

@ -0,0 +1,189 @@
/**
* BOGO Service - Handles Buy One Get One logic
*
* Processes BOGO rules from the API and calculates free items/discounts
*/
const BogoService = {
/**
* Check if a product has any BOGO rules
* @param {Object} product
* @returns {boolean}
*/
hasBogoRules(product) {
return product.bogo_rules && product.bogo_rules.length > 0;
},
/**
* Get the primary BOGO rule for a product (first one)
* @param {Object} product
* @returns {Object|null}
*/
getPrimaryBogoRule(product) {
if (!this.hasBogoRules(product)) return null;
return product.bogo_rules[0];
},
/**
* Get BOGO label for display
* @param {Object} product
* @returns {string|null}
*/
getBogoLabel(product) {
const rule = this.getPrimaryBogoRule(product);
return rule ? rule.label : null;
},
/**
* Calculate BOGO discount for cart items
*
* @param {Array} cartItems - Array of cart items with product data
* @param {Array} products - Full products list with BOGO rules
* @returns {Object} { freeItems: [], discountAmount: number }
*/
calculateBogoForCart(cartItems, products) {
const result = {
freeItems: [],
discountAmount: 0,
appliedRules: []
};
// Group cart items by product ID
const itemsByProduct = this.groupCartItemsByProduct(cartItems);
// Check each product group for BOGO eligibility
for (const [productId, items] of Object.entries(itemsByProduct)) {
const product = products.find(p => p.id == productId);
if (!product || !this.hasBogoRules(product)) continue;
const rule = this.getPrimaryBogoRule(product);
const quantity = items.length;
// Calculate how many free items based on the rule
const bogoResult = this.applyBogoRule(rule, quantity, product, products);
if (bogoResult.freeItems.length > 0 || bogoResult.discountAmount > 0) {
result.freeItems.push(...bogoResult.freeItems);
result.discountAmount += bogoResult.discountAmount;
result.appliedRules.push({
rule,
productId: parseInt(productId),
productName: product.name,
...bogoResult
});
}
}
return result;
},
/**
* Group cart items by product ID
* @param {Array} cartItems
* @returns {Object}
*/
groupCartItemsByProduct(cartItems) {
return cartItems.reduce((groups, item) => {
const id = item.id;
if (!groups[id]) groups[id] = [];
groups[id].push(item);
return groups;
}, {});
},
/**
* Apply a BOGO rule to calculate free items/discount
*
* @param {Object} rule - BOGO rule from API
* @param {number} quantity - Number of items in cart
* @param {Object} product - The product
* @param {Array} products - All products (for Buy X Get Y)
* @returns {Object}
*/
applyBogoRule(rule, quantity, product, products) {
const result = {
freeItems: [],
discountAmount: 0,
freeQuantity: 0
};
if (quantity < rule.buy_qty) {
return result; // Not enough items to trigger BOGO
}
// Calculate how many times the rule applies
let timesApplied = 1;
if (rule.recursive) {
timesApplied = Math.floor(quantity / rule.buy_qty);
}
const freeQty = timesApplied * rule.get_qty;
result.freeQuantity = freeQty;
if (rule.type === 'buy_x_get_x') {
// Same product is free or discounted
if (rule.discount_type === 'free_product' || rule.discount_value >= 100) {
// Fully free - automatically add to cart
result.discountAmount = freeQty * parseFloat(product.price);
result.freeItems.push({
id: product.id,
name: product.name + ' (GRATIS)',
price: '0.00',
originalPrice: product.price,
isFree: true,
ruleId: rule.rule_id
});
}
// For percentage and flat discounts, we DON'T automatically add items
// These are handled manually via addBogoDiscountedItem() in cart.js
// The user clicks a button to add the discounted item
} else if (rule.type === 'buy_x_get_y' && rule.get_product_ids) {
// Different product is free
for (const freeProductId of rule.get_product_ids) {
const freeProduct = products.find(p => p.id == freeProductId);
if (!freeProduct) continue;
if (rule.discount_type === 'free_product' || rule.discount_value >= 100) {
result.discountAmount += freeQty * parseFloat(freeProduct.price);
result.freeItems.push({
id: freeProduct.id,
name: freeProduct.name + ' (GRATIS)',
price: '0.00',
originalPrice: freeProduct.price,
isFree: true,
ruleId: rule.rule_id
});
}
}
}
return result;
},
/**
* Get suggested free items for a product (for display before adding to cart)
*
* @param {Object} product
* @param {Array} products
* @returns {Array}
*/
getSuggestedFreeItems(product, products) {
if (!this.hasBogoRules(product)) return [];
const rule = this.getPrimaryBogoRule(product);
const suggestions = [];
if (rule.type === 'buy_x_get_y' && rule.get_product_ids) {
for (const freeProductId of rule.get_product_ids) {
const freeProduct = products.find(p => p.id == freeProductId);
if (freeProduct) {
suggestions.push({
...freeProduct,
bogoLabel: `Gratis bij ${rule.buy_qty}x ${product.name}`
});
}
}
}
return suggestions;
}
};

266
logs/bogo_debug.log Normal file
View File

@ -0,0 +1,266 @@
[2026-01-10 16:49:41] DiscountRulesService initialized
Array
(
[tablePrefix] => wp_
[db_connected] => 1
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] buildProductBogoMap() called
Array
(
[productIds_count] => 39
[productIds_sample] => Array
(
[0] => 2483
[1] => 2376
[2] => 1903
[3] => 1831
[4] => 1794
)
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] fetchActiveBogoRules() called
Array
(
[table] => wp_wdr_rules
[currentTime] => 1768060181
[tablePrefix] => wp_
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Table wp_wdr_rules exists
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Executing query
SELECT
id,
title,
filters,
buy_x_get_x_adjustments,
buy_x_get_y_adjustments
FROM wp_wdr_rules
WHERE enabled = 1
AND deleted = 0
AND (date_from <= ? OR date_from IS NULL)
AND (date_to >= ? OR date_to IS NULL)
AND (usage_limits > used_limits OR used_limits IS NULL OR usage_limits = 0)
AND (
(buy_x_get_x_adjustments IS NOT NULL AND buy_x_get_x_adjustments != '{}' AND buy_x_get_x_adjustments != '[]')
OR (buy_x_get_y_adjustments IS NOT NULL AND buy_x_get_y_adjustments != '{}' AND buy_x_get_y_adjustments != '[]')
)
ORDER BY priority ASC
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Found 3 BOGO rules
Array
(
[0] => Array
(
[id] => 4
[title] => 1 + 1 GRATIS
[filters] => {"1":{"type":"products","method":"in_list","value":["1117","450","439","461","1110","406"],"product_variants":[],"product_variants_for_sale_badge":[]}}
[buy_x_get_x_adjustments] => {"ranges":{"1":{"from":"1","to":"1","free_qty":"1","free_type":"free_product","free_value":"100","recursive":"1"}}}
[buy_x_get_y_adjustments] => []
)
[1] => Array
(
[id] => 6
[title] => 2e voor 19,95
[filters] => {"1":{"type":"products","method":"in_list","value":["1831"],"product_variants":[],"product_variants_for_sale_badge":[]}}
[buy_x_get_x_adjustments] => {"ranges":{"1":{"from":"2","to":"","free_qty":"1","free_type":"flat","free_value":"10"}}}
[buy_x_get_y_adjustments] => []
)
[2] => Array
(
[id] => 7
[title] => Invictus One Prestige
[filters] => {"1":{"type":"products","method":"in_list","value":["1139","1466","1467"],"product_variants":[1467,1466],"product_variants_for_sale_badge":[1139]}}
[buy_x_get_x_adjustments] => []
[buy_x_get_y_adjustments] => {"type":"bxgy_product","operator":"product_cumulative","mode":"auto_add","ranges":{"1":{"from":"1","to":"1","products":["2376"],"free_qty":"1","free_type":"free_product","free_value":"","recursive":"1","product_varients":[],"product_variants_for_sale_badge":[]}}}
)
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Fetched rules
Array
(
[count] => 3
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Processing rule
Array
(
[id] => 4
[title] => 1 + 1 GRATIS
[filters] => {"1":{"type":"products","method":"in_list","value":["1117","450","439","461","1110","406"],"product_variants":[],"product_variants_for_sale_badge":[]}}
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Extracted filter product IDs
Array
(
[0] => 1117
[1] => 450
[2] => 439
[3] => 461
[4] => 1110
[5] => 406
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Parsed BOGO data
Array
(
[rule_id] => 4
[rule_title] => 1 + 1 GRATIS
[type] => buy_x_get_x
[label] => 1+1 Gratis
[buy_qty] => 1
[get_qty] => 1
[discount_type] => free_product
[discount_value] => 100
[recursive] => 1
[get_product_ids] =>
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Processing rule
Array
(
[id] => 6
[title] => 2e voor 19,95
[filters] => {"1":{"type":"products","method":"in_list","value":["1831"],"product_variants":[],"product_variants_for_sale_badge":[]}}
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Extracted filter product IDs
Array
(
[0] => 1831
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Parsed BOGO data
Array
(
[rule_id] => 6
[rule_title] => 2e voor 19,95
[type] => buy_x_get_x
[label] => Koop 2, krijg 1 met €10,00 korting
[buy_qty] => 2
[get_qty] => 1
[discount_type] => flat
[discount_value] => 10
[recursive] =>
[get_product_ids] =>
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Processing rule
Array
(
[id] => 7
[title] => Invictus One Prestige
[filters] => {"1":{"type":"products","method":"in_list","value":["1139","1466","1467"],"product_variants":[1467,1466],"product_variants_for_sale_badge":[1139]}}
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Extracted filter product IDs
Array
(
[0] => 1139
[1] => 1466
[2] => 1467
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Parsed BOGO data
Array
(
[rule_id] => 7
[rule_title] => Invictus One Prestige
[type] => buy_x_get_y
[label] => Koop 1, krijg 1 ander gratis
[buy_qty] => 1
[get_qty] => 1
[discount_type] => free_product
[discount_value] => 0
[recursive] => 1
[get_product_ids] => Array
(
[0] => 2376
)
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Final product map
Array
(
[products_with_bogo] => 10
[sample] => Array
(
[1117] => Array
(
[0] => Array
(
[rule_id] => 4
[rule_title] => 1 + 1 GRATIS
[type] => buy_x_get_x
[label] => 1+1 Gratis
[buy_qty] => 1
[get_qty] => 1
[discount_type] => free_product
[discount_value] => 100
[recursive] => 1
[get_product_ids] =>
)
)
[450] => Array
(
[0] => Array
(
[rule_id] => 4
[rule_title] => 1 + 1 GRATIS
[type] => buy_x_get_x
[label] => 1+1 Gratis
[buy_qty] => 1
[get_qty] => 1
[discount_type] => free_product
[discount_value] => 100
[recursive] => 1
[get_product_ids] =>
)
)
[439] => Array
(
[0] => Array
(
[rule_id] => 4
[rule_title] => 1 + 1 GRATIS
[type] => buy_x_get_x
[label] => 1+1 Gratis
[buy_qty] => 1
[get_qty] => 1
[discount_type] => free_product
[discount_value] => 100
[recursive] => 1
[get_product_ids] =>
)
)
)
)
--------------------------------------------------------------------------------

35
logs/products_debug.log Normal file
View File

@ -0,0 +1,35 @@
[2026-01-10 16:49:40] handleGetProducts() started
--------------------------------------------------------------------------------
[2026-01-10 16:49:40] WooCommerce client initialized
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Fetched products from WooCommerce
Array
(
[count] => 39
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Built CUW map
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Creating DiscountRulesService...
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] DiscountRulesService created
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Product IDs extracted
Array
(
[count] => 39
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] Calling buildProductBogoMap...
--------------------------------------------------------------------------------
[2026-01-10 16:49:41] BOGO map built
Array
(
[products_with_bogo] => 10
)
--------------------------------------------------------------------------------
[2026-01-10 16:49:46] Returning 39 enriched products
--------------------------------------------------------------------------------