543 lines
17 KiB
PHP
543 lines
17 KiB
PHP
<?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 [];
|
|
}
|
|
}
|
|
}
|