feat(Core): add optimize and complete code

This commit is contained in:
2026-05-17 20:05:15 +03:30
parent aa944bf339
commit 4e60b7efdd
25 changed files with 858 additions and 54 deletions

View File

@@ -37,6 +37,9 @@ class AdminController {
'cart_contains_category',
'customer_order_count_min',
'customer_order_count_max',
'customer_days_since_last_order_min',
'product_total_sales_max',
'product_total_sales_min',
'day_of_week',
];
private $allowedRuleConditionOperators = ['is', 'is_not', 'in', 'not_in'];
@@ -81,6 +84,15 @@ class AdminController {
[$this, 'addRulePage']
);
add_submenu_page(
'sodino-rules',
__('قالب‌های آماده', 'sodino'),
__('قالب‌های آماده', 'sodino'),
'manage_options',
'sodino-templates',
[$this, 'templatesPage']
);
add_submenu_page(
'sodino-rules',
__('آپسل (پیشنهاد فروش)', 'sodino'),
@@ -199,6 +211,12 @@ class AdminController {
$this->listRulesPage();
}
public function templatesPage() {
$ruleTemplates = $this->getRuleTemplates();
$upsellTemplates = $this->getUpsellTemplates();
include SODINO_PLUGIN_DIR . 'admin/views/templates.php';
}
/**
* Dashboard page
*/
@@ -249,6 +267,8 @@ class AdminController {
$this->saveRule();
} else {
$rule = new Rule();
$templateKey = isset($_GET['template']) ? sanitize_key(wp_unslash($_GET['template'])) : '';
$selectedTemplate = $this->applyRuleTemplate($rule, $templateKey);
include SODINO_PLUGIN_DIR . 'admin/views/rule-form.php';
}
}
@@ -284,6 +304,8 @@ class AdminController {
$this->saveUpsell();
} else {
$upsell = new Upsell();
$templateKey = isset($_GET['template']) ? sanitize_key(wp_unslash($_GET['template'])) : '';
$selectedTemplate = $this->applyUpsellTemplate($upsell, $templateKey);
include SODINO_PLUGIN_DIR . 'admin/views/upsell-form.php';
}
}
@@ -371,6 +393,117 @@ class AdminController {
include SODINO_PLUGIN_DIR . 'admin/views/banner-list.php';
}
private function getRuleTemplates() {
return [
'first_purchase' => [
'title' => __('تخفیف اولین خرید', 'sodino'),
'description' => __('برای کاربران واردشده‌ای که هنوز سفارشی ثبت نکرده‌اند تخفیف درصدی اعمال می‌کند.', 'sodino'),
'name' => __('تخفیف اولین خرید', 'sodino'),
'priority' => 90,
'usage_limit' => 0,
'conditions' => [
['type' => 'user_type', 'operator' => 'is', 'value' => 'new'],
],
'actions' => [
['type' => 'discount_percent', 'value' => 10],
],
],
'return_after_30_days' => [
'title' => __('تخفیف بازگشت مشتری بعد از ۳۰ روز', 'sodino'),
'description' => __('برای مشتریانی که حداقل یک سفارش دارند و ۳۰ روز از آخرین سفارششان گذشته است.', 'sodino'),
'name' => __('کمپین بازگشت مشتریان قدیمی', 'sodino'),
'priority' => 80,
'usage_limit' => 0,
'conditions' => [
['type' => 'customer_order_count_min', 'operator' => 'is', 'value' => '1'],
['type' => 'customer_days_since_last_order_min', 'operator' => 'is', 'value' => '30'],
],
'actions' => [
['type' => 'discount_percent', 'value' => 12],
],
],
'cart_total_threshold' => [
'title' => __('تخفیف سبد بالای مبلغ مشخص', 'sodino'),
'description' => __('وقتی مبلغ سبد از حد مشخصی بالاتر رفت، مشتری تخفیف دریافت می‌کند.', 'sodino'),
'name' => __('تخفیف سبد خرید بالای ۱٬۰۰۰٬۰۰۰', 'sodino'),
'priority' => 70,
'usage_limit' => 0,
'conditions' => [
['type' => 'cart_total_min', 'operator' => 'is', 'value' => '1000000'],
],
'actions' => [
['type' => 'discount_percent', 'value' => 5],
],
],
'slow_moving_stock' => [
'title' => __('فروش سریع موجودی کم‌فروش', 'sodino'),
'description' => __('برای محصولاتی که فروش کل پایینی دارند تخفیف خودکار می‌گذارد.', 'sodino'),
'name' => __('تخفیف محصولات کم‌فروش', 'sodino'),
'priority' => 60,
'usage_limit' => 0,
'conditions' => [
['type' => 'product_total_sales_max', 'operator' => 'is', 'value' => '5'],
],
'actions' => [
['type' => 'discount_percent', 'value' => 15],
],
],
];
}
private function getUpsellTemplates() {
return [
'complementary_cart_product' => [
'title' => __('پیشنهاد محصول مکمل در سبد خرید', 'sodino'),
'description' => __('وقتی محصول یا دسته فعال‌ساز در سبد باشد، محصول مکمل را با تخفیف پیشنهاد می‌دهد.', 'sodino'),
'name' => __('پیشنهاد محصول مکمل', 'sodino'),
'trigger_type' => 'product',
'trigger_value' => '',
'target_product_id' => 0,
'discount_type' => 'percentage',
'discount_value' => 10,
'priority' => 80,
],
];
}
private function applyRuleTemplate(Rule $rule, $templateKey) {
$templates = $this->getRuleTemplates();
if (empty($templateKey) || empty($templates[$templateKey])) {
return null;
}
$template = $templates[$templateKey];
$rule->name = $template['name'];
$rule->priority = (int) $template['priority'];
$rule->usage_limit = (int) $template['usage_limit'];
$rule->conditions = $template['conditions'];
$rule->actions = $template['actions'];
$rule->enabled = 1;
$rule->syncLegacyFields();
return $template;
}
private function applyUpsellTemplate(Upsell $upsell, $templateKey) {
$templates = $this->getUpsellTemplates();
if (empty($templateKey) || empty($templates[$templateKey])) {
return null;
}
$template = $templates[$templateKey];
$upsell->title = $template['name'];
$upsell->trigger_type = $template['trigger_type'];
$upsell->trigger_value = $template['trigger_value'];
$upsell->target_product_id = (int) $template['target_product_id'];
$upsell->discount_type = $template['discount_type'];
$upsell->discount_value = (float) $template['discount_value'];
$upsell->priority = (int) $template['priority'];
$upsell->status = 1;
return $template;
}
private function editBannerPage() {
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$banner = $this->bannerRepository->getById($id);
@@ -522,7 +655,10 @@ class AdminController {
return;
}
if (!isset($_GET['_wpnonce']) || !in_array($_GET['action'], ['delete_upsell', 'toggle_upsell_status'], true) || !wp_verify_nonce($_GET['_wpnonce'], $_GET['action'])) {
$action = isset($_GET['action']) ? sanitize_key(wp_unslash($_GET['action'])) : '';
$nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
if (!in_array($action, ['delete_upsell', 'toggle_upsell_status'], true) || !wp_verify_nonce($nonce, $action)) {
return;
}
@@ -531,13 +667,13 @@ class AdminController {
return;
}
if ($_GET['action'] === 'delete_upsell') {
if ($action === 'delete_upsell') {
$this->upsellRepository->delete($id);
wp_safe_redirect(admin_url('admin.php?page=sodino-upsells'));
exit;
}
if ($_GET['action'] === 'toggle_upsell_status') {
if ($action === 'toggle_upsell_status') {
$upsell = $this->upsellRepository->getById($id);
if ($upsell) {
$upsell->status = $upsell->status ? 0 : 1;
@@ -553,7 +689,10 @@ class AdminController {
return;
}
if (!isset($_GET['_wpnonce']) || !in_array($_GET['action'], ['delete_banner', 'toggle_banner_status'], true) || !wp_verify_nonce($_GET['_wpnonce'], $_GET['action'])) {
$action = isset($_GET['action']) ? sanitize_key(wp_unslash($_GET['action'])) : '';
$nonce = isset($_GET['_wpnonce']) ? sanitize_text_field(wp_unslash($_GET['_wpnonce'])) : '';
if (!in_array($action, ['delete_banner', 'toggle_banner_status'], true) || !wp_verify_nonce($nonce, $action)) {
return;
}
@@ -562,13 +701,13 @@ class AdminController {
return;
}
if ($_GET['action'] === 'delete_banner') {
if ($action === 'delete_banner') {
$this->bannerRepository->delete($id);
wp_safe_redirect(admin_url('admin.php?page=sodino-banners'));
exit;
}
if ($_GET['action'] === 'toggle_banner_status') {
if ($action === 'toggle_banner_status') {
$banner = $this->bannerRepository->getById($id);
if ($banner) {
$banner->status = $banner->status ? 0 : 1;
@@ -588,7 +727,7 @@ class AdminController {
wp_send_json([]);
}
$term = sanitize_text_field($_POST['term'] ?? '');
$term = isset($_POST['term']) ? sanitize_text_field(wp_unslash($_POST['term'])) : '';
if (empty($term) || !function_exists('wc_get_products')) {
wp_send_json([]);
}

View File

@@ -20,6 +20,10 @@ class Cache {
* Get cached value
*/
public function get($key, $group = 'sodino') {
if (!$this->isEnabled()) {
return false;
}
$full_key = $this->buildKey($key, $group);
// Check memory cache first
@@ -42,6 +46,10 @@ class Cache {
* Set cached value
*/
public function set($key, $value, $expiration = 3600, $group = 'sodino') {
if (!$this->isEnabled()) {
return false;
}
$full_key = $this->buildKey($key, $group);
// Set in memory cache
@@ -134,4 +142,12 @@ class Cache {
private function buildKey($key, $group) {
return "sodino_{$group}_{$key}";
}
private function isEnabled() {
if (!class_exists(__NAMESPACE__ . '\Settings')) {
return true;
}
return Settings::getInstance()->isCacheEnabled();
}
}

View File

@@ -73,14 +73,12 @@ class BannerRepository {
public function incrementImpression($id) {
global $wpdb;
$wpdb->query($wpdb->prepare("UPDATE {$this->table_name} SET impressions = impressions + 1 WHERE id = %d", $id));
$this->clearCache();
return $wpdb->query($wpdb->prepare("UPDATE {$this->table_name} SET impressions = impressions + 1 WHERE id = %d", $id));
}
public function incrementClick($id) {
global $wpdb;
$wpdb->query($wpdb->prepare("UPDATE {$this->table_name} SET clicks = clicks + 1 WHERE id = %d", $id));
$this->clearCache();
return $wpdb->query($wpdb->prepare("UPDATE {$this->table_name} SET clicks = clicks + 1 WHERE id = %d", $id));
}
public function clearCache() {

View File

@@ -23,7 +23,7 @@ class EventRepository {
$where = $this->buildWhereClauses($filters, $params);
$sql = "SELECT * FROM {$this->table_name} WHERE " . implode(' AND ', $where) . " ORDER BY created_at ASC";
return $wpdb->get_results($wpdb->prepare($sql, $params), ARRAY_A);
return $wpdb->get_results($this->prepareSql($sql, $params), ARRAY_A);
}
public function getCount(array $filters = []) {
@@ -32,7 +32,7 @@ class EventRepository {
$where = $this->buildWhereClauses($filters, $params);
$sql = "SELECT COUNT(*) FROM {$this->table_name} WHERE " . implode(' AND ', $where);
return (int) $wpdb->get_var($wpdb->prepare($sql, $params));
return (int) $wpdb->get_var($this->prepareSql($sql, $params));
}
public function getSum($field, array $filters = []) {
@@ -45,7 +45,7 @@ class EventRepository {
$where = $this->buildWhereClauses($filters, $params);
$sql = "SELECT SUM({$field}) FROM {$this->table_name} WHERE " . implode(' AND ', $where);
return floatval($wpdb->get_var($wpdb->prepare($sql, $params)));
return floatval($wpdb->get_var($this->prepareSql($sql, $params)));
}
public function getRuleUsageCount($rule_id) {
@@ -95,4 +95,14 @@ class EventRepository {
return $where;
}
private function prepareSql($sql, array $params) {
global $wpdb;
if (empty($params)) {
return $sql;
}
return $wpdb->prepare($sql, $params);
}
}

View File

@@ -126,12 +126,18 @@ class RuleRepository {
*/
public function incrementUsage($id) {
global $wpdb;
return $wpdb->query(
$result = $wpdb->query(
$wpdb->prepare(
"UPDATE {$this->table_name} SET usage_count = usage_count + 1 WHERE id = %d",
$id
)
);
if ($result !== false) {
$this->clearCache();
}
return $result;
}
/**

View File

@@ -44,13 +44,19 @@ class AnalyticsService {
$summary = $this->getSummary($filters);
$salesChart = $this->getSalesChart($filters);
$rulePerformance = $this->getRulePerformance($filters);
$roiReport = $this->getRoiReport($filters);
$upsellPerformance = $this->getUpsellPerformance($filters);
$bannerPerformance = $this->getBannerPerformance($filters, $summary);
$userBehavior = $this->getUserBehavior($filters);
$insights = $this->getInsights($summary, $filters);
$insights = $this->getInsights($summary, $filters, $roiReport);
$result = [
'summary' => $summary,
'sales_chart' => $salesChart,
'rule_performance' => $rulePerformance,
'roi_report' => $roiReport,
'upsell_performance' => $upsellPerformance,
'banner_performance' => $bannerPerformance,
'user_behavior' => $userBehavior,
'insights' => $insights,
];
@@ -152,6 +158,160 @@ class AnalyticsService {
return $result;
}
public function getRoiReport(array $filters = []) {
$orders = $this->getOrdersInRange($filters);
$attributedRevenue = 0;
$attributedDiscount = 0;
$attributedOrders = [];
$ruleRows = [];
$upsellRows = [];
foreach ($orders as $order) {
$orderId = $order->get_id();
foreach ($order->get_items() as $item) {
$lineRevenue = (float) $item->get_total();
$ruleIds = $this->parseIdList($item->get_meta('_sodino_rule_ids', true));
$ruleDiscount = (float) $item->get_meta('_sodino_rule_discount', true);
$upsellId = (int) $item->get_meta('_sodino_upsell_id', true);
if (!empty($ruleIds) || $upsellId > 0) {
$attributedRevenue += $lineRevenue;
$attributedDiscount += $ruleDiscount;
$attributedOrders[$orderId] = true;
}
foreach ($ruleIds as $ruleId) {
if (!isset($ruleRows[$ruleId])) {
$ruleRows[$ruleId] = [
'rule_id' => $ruleId,
'name' => $this->getRuleName($ruleId),
'orders' => [],
'revenue' => 0,
'discount' => 0,
];
}
$allocation = count($ruleIds) > 0 ? $lineRevenue / count($ruleIds) : $lineRevenue;
$discountAllocation = count($ruleIds) > 0 ? $ruleDiscount / count($ruleIds) : $ruleDiscount;
$ruleRows[$ruleId]['orders'][$orderId] = true;
$ruleRows[$ruleId]['revenue'] += $allocation;
$ruleRows[$ruleId]['discount'] += $discountAllocation;
}
if ($upsellId > 0) {
if (!isset($upsellRows[$upsellId])) {
$upsellRows[$upsellId] = [
'upsell_id' => $upsellId,
'title' => $this->getUpsellTitle($upsellId),
'orders' => [],
'revenue' => 0,
];
}
$upsellRows[$upsellId]['orders'][$orderId] = true;
$upsellRows[$upsellId]['revenue'] += $lineRevenue;
}
}
}
$ruleRows = array_map(function ($row) {
$row['order_count'] = count($row['orders']);
unset($row['orders']);
$row['revenue'] = round($row['revenue'], 2);
$row['discount'] = round($row['discount'], 2);
return $row;
}, array_values($ruleRows));
usort($ruleRows, function ($a, $b) {
return $b['revenue'] <=> $a['revenue'];
});
$upsellRows = array_map(function ($row) {
$row['order_count'] = count($row['orders']);
unset($row['orders']);
$row['revenue'] = round($row['revenue'], 2);
return $row;
}, array_values($upsellRows));
usort($upsellRows, function ($a, $b) {
return $b['revenue'] <=> $a['revenue'];
});
return [
'attributed_revenue' => round($attributedRevenue, 2),
'attributed_discount' => round($attributedDiscount, 2),
'attributed_order_count' => count($attributedOrders),
'rule_rows' => $ruleRows,
'upsell_rows' => $upsellRows,
];
}
public function getUpsellPerformance(array $filters = []) {
$roi = $this->getRoiReport($filters);
$orderRows = [];
foreach ($roi['upsell_rows'] as $row) {
$orderRows[(int) $row['upsell_id']] = $row;
}
$repository = new \Sodino\Repositories\UpsellRepository();
$rows = [];
foreach ($repository->getAll() as $upsell) {
$orderRow = $orderRows[(int) $upsell->id] ?? null;
$impressions = max(0, (int) $upsell->impressions);
$addToCartConversions = max(0, (int) $upsell->conversions);
$orderCount = $orderRow ? (int) $orderRow['order_count'] : 0;
$revenue = $orderRow ? (float) $orderRow['revenue'] : 0;
$rows[] = [
'id' => (int) $upsell->id,
'title' => $upsell->title,
'impressions' => $impressions,
'add_to_cart' => $addToCartConversions,
'orders' => $orderCount,
'revenue' => round($revenue, 2),
'cart_conversion_rate' => $impressions > 0 ? round(($addToCartConversions / $impressions) * 100, 2) : 0,
'order_conversion_rate' => $impressions > 0 ? round(($orderCount / $impressions) * 100, 2) : 0,
];
}
usort($rows, function ($a, $b) {
return $b['revenue'] <=> $a['revenue'];
});
return $rows;
}
public function getBannerPerformance(array $filters = [], array $summary = []) {
$repository = new \Sodino\Repositories\BannerRepository();
$purchaseCount = (int) ($summary['purchase_count'] ?? $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'purchase'])));
$clickTotal = 0;
foreach ($repository->getAll() as $banner) {
$clickTotal += max(0, (int) $banner->clicks);
}
$rows = [];
foreach ($repository->getAll() as $banner) {
$impressions = max(0, (int) $banner->impressions);
$clicks = max(0, (int) $banner->clicks);
$estimatedOrders = $clickTotal > 0 ? round(($clicks / $clickTotal) * $purchaseCount, 2) : 0;
$rows[] = [
'id' => (int) $banner->id,
'title' => $banner->title,
'impressions' => $impressions,
'clicks' => $clicks,
'ctr' => $impressions > 0 ? round(($clicks / $impressions) * 100, 2) : 0,
'estimated_orders' => $estimatedOrders,
];
}
usort($rows, function ($a, $b) {
return $b['clicks'] <=> $a['clicks'];
});
return $rows;
}
public function getUserBehavior(array $filters = []) {
$productViewCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'product_view']));
$addToCartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'add_to_cart']));
@@ -179,9 +339,16 @@ class AnalyticsService {
return $performance[0]['name'] ?? null;
}
private function getInsights(array $summary, array $filters = []) {
private function getInsights(array $summary, array $filters = [], array $roiReport = []) {
$insights = [];
if (!empty($roiReport['attributed_revenue'])) {
$insights[] = sprintf(
__('سودینو %s فروش اثرگرفته ثبت کرده است.', 'sodino'),
wp_strip_all_tags(wc_price((float) $roiReport['attributed_revenue']))
);
}
if (!empty($summary['best_rule'])) {
$insights[] = sprintf('%s %s', __('قانون برتر:', 'sodino'), esc_html($summary['best_rule']));
}
@@ -208,24 +375,74 @@ class AnalyticsService {
}
private function getDateRange($range, $start, $end) {
$today = current_time('Y-m-d');
$result = [
'start' => date('Y-m-d', strtotime('-6 days')),
'end' => date('Y-m-d'),
'start' => date('Y-m-d', strtotime($today . ' -6 days')),
'end' => $today,
];
if ($range === '30d') {
$result['start'] = date('Y-m-d', strtotime('-29 days'));
$result['end'] = date('Y-m-d');
$result['start'] = date('Y-m-d', strtotime($today . ' -29 days'));
$result['end'] = $today;
}
if ($range === 'custom' && !empty($start) && !empty($end)) {
$result['start'] = date('Y-m-d', strtotime($start));
$result['end'] = date('Y-m-d', strtotime($end));
$startTimestamp = strtotime($start);
$endTimestamp = strtotime($end);
if ($startTimestamp && $endTimestamp) {
if ($endTimestamp < $startTimestamp) {
$endTimestamp = $startTimestamp;
}
$result['start'] = date('Y-m-d', $startTimestamp);
$result['end'] = date('Y-m-d', $endTimestamp);
}
}
return $result;
}
private function getOrdersInRange(array $filters = []) {
if (!function_exists('wc_get_orders')) {
return [];
}
$range = $this->getDateRange($filters['range'] ?? '7d', $filters['start_date'] ?? '', $filters['end_date'] ?? '');
$start = $filters['from'] ?? $range['start'];
$end = $filters['to'] ?? $range['end'];
return wc_get_orders([
'limit' => -1,
'status' => ['completed', 'processing'],
'date_created' => $start . '...' . $end,
'return' => 'objects',
]);
}
private function parseIdList($value) {
$ids = [];
foreach (explode(',', (string) $value) as $id) {
$id = absint(trim($id));
if ($id > 0) {
$ids[] = $id;
}
}
return array_values(array_unique($ids));
}
private function getRuleName($ruleId) {
$rule = $this->ruleRepository->getById((int) $ruleId);
return $rule ? $rule->name : sprintf(__('قانون #%d', 'sodino'), (int) $ruleId);
}
private function getUpsellTitle($upsellId) {
$repository = new \Sodino\Repositories\UpsellRepository();
$upsell = $repository->getById((int) $upsellId);
return $upsell ? $upsell->title : sprintf(__('آپسل #%d', 'sodino'), (int) $upsellId);
}
public function getProductIdsByCategory($category_id) {
$products = get_posts([
'post_type' => 'product',

View File

@@ -112,6 +112,12 @@ class BannerService {
wp_cache_set('version', $version, 'sodino_banners');
}
return 'sodino_active_banners_' . md5($version . '|' . $position . '|' . serialize($context));
$runtimeContext = [
'user' => is_user_logged_in() ? 'returning' : 'new',
'device' => wp_is_mobile() ? 'mobile' : 'desktop',
'minute' => gmdate('YmdHi', current_time('timestamp', true)),
];
return 'sodino_active_banners_' . md5($version . '|' . $position . '|' . wp_json_encode($context) . '|' . wp_json_encode($runtimeContext));
}
}

View File

@@ -11,7 +11,8 @@ class PricingService {
private $trackingService;
private $settings;
private $cache;
private $trackedApplications = [];
private $appliedRules = [];
private $trackedConversions = [];
public function __construct(RuleRepository $ruleRepository, TrackingService $trackingService) {
$this->ruleRepository = $ruleRepository;
@@ -29,7 +30,14 @@ class PricingService {
return $price;
}
if (!$product || !is_a($product, 'WC_Product')) {
return $price;
}
$price = $this->normalizePrice($price);
if ($price <= 0) {
return $price;
}
if (!$this->settings->get('cart_pricing_enabled') && is_cart()) {
return $price;
@@ -50,8 +58,7 @@ class PricingService {
foreach ($rules as $rule) {
$oldPrice = $price;
$price = $this->applyRuleActions($rule, $price);
$this->trackDiscountOnce($product, $oldPrice, $price, $rule->id);
$this->rememberAppliedRule($product, $oldPrice, $price, $rule->id);
}
$price = $this->enforceLimits($originalPrice, $price);
@@ -59,6 +66,56 @@ class PricingService {
return max(0, $price);
}
public function getAppliedRulesForProduct($product) {
if (!$product || !is_a($product, 'WC_Product')) {
return [];
}
$keys = $this->getProductTrackingKeys($product);
$rules = [];
foreach ($keys as $key) {
if (!empty($this->appliedRules[$key])) {
$rules = array_merge($rules, $this->appliedRules[$key]);
}
}
if (empty($rules)) {
return [];
}
$unique = [];
foreach ($rules as $rule) {
$unique[(int) $rule['rule_id']] = $rule;
}
return array_values($unique);
}
public function trackAppliedRulesForProduct($product) {
foreach ($this->getAppliedRulesForProduct($product) as $rule) {
$trackingKey = implode(':', [
(int) $product->get_id(),
(int) $rule['rule_id'],
round((float) $rule['original_price'], 4),
round((float) $rule['discounted_price'], 4),
]);
if (isset($this->trackedConversions[$trackingKey])) {
continue;
}
$this->trackedConversions[$trackingKey] = true;
$this->trackingService->recordDiscountApplied(
$product,
(float) $rule['original_price'],
(float) $rule['discounted_price'],
(int) $rule['rule_id']
);
$this->ruleRepository->incrementUsage((int) $rule['rule_id']);
}
}
private function getApplicableRules($product) {
$rules = $this->ruleRepository->getEnabled();
$applicable = [];
@@ -160,6 +217,12 @@ class PricingService {
return $this->getCustomerOrderCount() >= intval($value);
case 'customer_order_count_max':
return $this->getCustomerOrderCount() <= intval($value);
case 'customer_days_since_last_order_min':
return $this->getCustomerDaysSinceLastOrder() >= intval($value);
case 'product_total_sales_max':
return $this->getProductTotalSales($product) <= intval($value);
case 'product_total_sales_min':
return $this->getProductTotalSales($product) >= intval($value);
case 'day_of_week':
return in_array((string) current_time('N'), array_map('strval', $this->normalizeIdList($value)), true);
default:
@@ -195,6 +258,35 @@ class PricingService {
return (int) wc_get_customer_order_count(get_current_user_id());
}
private function getCustomerDaysSinceLastOrder() {
if (!is_user_logged_in()) {
return 0;
}
$orders = wc_get_orders([
'customer_id' => get_current_user_id(),
'limit' => 1,
'orderby' => 'date',
'order' => 'DESC',
'status' => ['wc-completed', 'wc-processing'],
'return' => 'objects',
]);
if (empty($orders)) {
return 0;
}
$date = $orders[0]->get_date_created();
if (!$date) {
return 0;
}
$lastOrderTimestamp = $date->getTimestamp();
$now = current_time('timestamp', true);
return max(0, (int) floor(($now - $lastOrderTimestamp) / DAY_IN_SECONDS));
}
private function userHasAllowedRole($roles) {
if (!is_user_logged_in()) {
return false;
@@ -227,12 +319,36 @@ class PricingService {
return $this->productHasTerm($product, $categories, 'product_cat');
}
private function getProductTotalSales($product) {
if (!$product || !is_a($product, 'WC_Product')) {
return 0;
}
$productId = (int) $product->get_id();
if ($product->is_type('variation') && method_exists($product, 'get_parent_id')) {
$parentId = (int) $product->get_parent_id();
if ($parentId > 0) {
$productId = $parentId;
}
}
return (int) get_post_meta($productId, 'total_sales', true);
}
private function productHasTerm($product, $terms, $taxonomy) {
if (!$product || empty($terms)) {
return false;
}
$product_terms = wp_get_post_terms($product->get_id(), $taxonomy, ['fields' => 'ids']);
$productId = (int) $product->get_id();
if ($product->is_type('variation') && method_exists($product, 'get_parent_id')) {
$parentId = (int) $product->get_parent_id();
if ($parentId > 0) {
$productId = $parentId;
}
}
$product_terms = wp_get_post_terms($productId, $taxonomy, ['fields' => 'ids']);
if (is_wp_error($product_terms)) {
return false;
}
@@ -428,16 +544,30 @@ class PricingService {
return false;
}
private function trackDiscountOnce($product, $oldPrice, $price, $ruleId) {
$productId = $product ? $product->get_id() : 0;
$key = implode(':', [$productId, (int) $ruleId, round($oldPrice, 4), round($price, 4)]);
if ($price >= $oldPrice || isset($this->trackedApplications[$key])) {
private function rememberAppliedRule($product, $oldPrice, $price, $ruleId) {
if ($price >= $oldPrice || !$product || !is_a($product, 'WC_Product')) {
return;
}
$this->trackedApplications[$key] = true;
$this->trackingService->recordDiscountApplied($product, $oldPrice, $price, $ruleId);
$this->ruleRepository->incrementUsage($ruleId);
foreach ($this->getProductTrackingKeys($product) as $key) {
$this->appliedRules[$key][(int) $ruleId] = [
'rule_id' => (int) $ruleId,
'original_price' => (float) $oldPrice,
'discounted_price' => (float) $price,
];
}
}
private function getProductTrackingKeys($product) {
$keys = [(int) $product->get_id()];
if ($product->is_type('variation') && method_exists($product, 'get_parent_id')) {
$parentId = (int) $product->get_parent_id();
if ($parentId > 0) {
$keys[] = $parentId;
}
}
return array_values(array_unique($keys));
}
}

View File

@@ -98,6 +98,10 @@ class TrackingService {
}
private function logEvent($type, array $data = []) {
if (empty($type)) {
return;
}
$event = [
'event_type' => $type,
'product_id' => isset($data['product_id']) ? intval($data['product_id']) : null,
@@ -111,7 +115,7 @@ class TrackingService {
'created_at' => current_time('mysql'),
];
$this->eventRepository->insert($event);
return $this->eventRepository->insert($event);
}
private function getSessionId() {
@@ -124,7 +128,10 @@ class TrackingService {
return $session_id;
}
return 'guest_' . md5($_SERVER['REMOTE_ADDR'] . '|' . $_SERVER['HTTP_USER_AGENT']);
$remote_addr = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '';
$user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
return 'guest_' . md5($remote_addr . '|' . $user_agent);
}
private function hasLogged($key) {