Discountrules added
This commit is contained in:
parent
79d4861e6d
commit
cadd9d962e
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
|
||||||
|
|||||||
@ -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,7 +90,7 @@ 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,
|
||||||
@ -60,9 +98,12 @@ function handleGetProducts(): void
|
|||||||
$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()]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
542
api/services/DiscountRulesService.php
Normal file
542
api/services/DiscountRulesService.php
Normal 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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
index.html
70
index.html
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,25 +31,33 @@ 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);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,16 +65,179 @@ const CartComponent = {
|
|||||||
* @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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
189
js/services/bogo.js
Normal 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
266
logs/bogo_debug.log
Normal 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
35
logs/products_debug.log
Normal 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
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
Loading…
x
Reference in New Issue
Block a user