feat(Core): add optimize and complete code
This commit is contained in:
@@ -146,6 +146,15 @@ add_action('admin_menu', function() use ($adminController) {
|
|||||||
[$adminController, 'addRulePage']
|
[$adminController, 'addRulePage']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-dashboard',
|
||||||
|
__('قالبهای آماده', 'sodino'),
|
||||||
|
__('قالبهای آماده', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-templates',
|
||||||
|
[$adminController, 'templatesPage']
|
||||||
|
);
|
||||||
|
|
||||||
add_submenu_page(
|
add_submenu_page(
|
||||||
'sodino-dashboard',
|
'sodino-dashboard',
|
||||||
__('آپسل (پیشنهاد فروش)', 'sodino'),
|
__('آپسل (پیشنهاد فروش)', 'sodino'),
|
||||||
@@ -247,8 +256,8 @@ add_action('admin_enqueue_scripts', function($hook) {
|
|||||||
* Handle admin actions
|
* Handle admin actions
|
||||||
*/
|
*/
|
||||||
add_action('admin_init', function() use ($ruleController, $settingsController, $adminController) {
|
add_action('admin_init', function() use ($ruleController, $settingsController, $adminController) {
|
||||||
$page = $_GET['page'] ?? '';
|
$page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : '';
|
||||||
$action = $_GET['action'] ?? '';
|
$action = isset($_GET['action']) ? sanitize_key(wp_unslash($_GET['action'])) : '';
|
||||||
|
|
||||||
// Rule actions
|
// Rule actions
|
||||||
if ($page === 'sodino-rules' && $action === 'delete') {
|
if ($page === 'sodino-rules' && $action === 'delete') {
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ class Sodino_Banner_List_Table extends WP_List_Table {
|
|||||||
|
|
||||||
public function process_bulk_action() {
|
public function process_bulk_action() {
|
||||||
if ('delete' === $this->current_action()) {
|
if ('delete' === $this->current_action()) {
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$banner_ids = isset($_POST['banner_ids']) ? array_map('intval', $_POST['banner_ids']) : [];
|
$banner_ids = isset($_POST['banner_ids']) ? array_map('intval', $_POST['banner_ids']) : [];
|
||||||
if (!empty($banner_ids) && check_admin_referer('bulk-' . $this->_args['plural'])) {
|
if (!empty($banner_ids) && check_admin_referer('bulk-' . $this->_args['plural'])) {
|
||||||
foreach ($banner_ids as $id) {
|
foreach ($banner_ids as $id) {
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ class Sodino_Rules_List_Table extends WP_List_Table {
|
|||||||
'cart_contains_category' => __('سبد شامل دستهبندی', 'sodino'),
|
'cart_contains_category' => __('سبد شامل دستهبندی', 'sodino'),
|
||||||
'customer_order_count_min' => __('حداقل سفارش مشتری', 'sodino'),
|
'customer_order_count_min' => __('حداقل سفارش مشتری', 'sodino'),
|
||||||
'customer_order_count_max' => __('حداکثر سفارش مشتری', 'sodino'),
|
'customer_order_count_max' => __('حداکثر سفارش مشتری', 'sodino'),
|
||||||
|
'customer_days_since_last_order_min' => __('حداقل روز از آخرین سفارش', 'sodino'),
|
||||||
|
'product_total_sales_max' => __('حداکثر فروش کل محصول', 'sodino'),
|
||||||
|
'product_total_sales_min' => __('حداقل فروش کل محصول', 'sodino'),
|
||||||
'day_of_week' => __('روز هفته', 'sodino'),
|
'day_of_week' => __('روز هفته', 'sodino'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -171,6 +174,10 @@ class Sodino_Rules_List_Table extends WP_List_Table {
|
|||||||
|
|
||||||
public function process_bulk_action() {
|
public function process_bulk_action() {
|
||||||
if ('delete' === $this->current_action()) {
|
if ('delete' === $this->current_action()) {
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$rule_ids = isset($_POST['rule_ids']) ? array_map('intval', $_POST['rule_ids']) : [];
|
$rule_ids = isset($_POST['rule_ids']) ? array_map('intval', $_POST['rule_ids']) : [];
|
||||||
if (!empty($rule_ids) && check_admin_referer('bulk-' . $this->_args['plural'])) {
|
if (!empty($rule_ids) && check_admin_referer('bulk-' . $this->_args['plural'])) {
|
||||||
foreach ($rule_ids as $id) {
|
foreach ($rule_ids as $id) {
|
||||||
|
|||||||
@@ -144,6 +144,10 @@ class Sodino_Upsell_List_Table extends WP_List_Table {
|
|||||||
|
|
||||||
public function process_bulk_action() {
|
public function process_bulk_action() {
|
||||||
if ('delete' === $this->current_action()) {
|
if ('delete' === $this->current_action()) {
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$upsell_ids = isset($_POST['upsell_ids']) ? array_map('intval', $_POST['upsell_ids']) : [];
|
$upsell_ids = isset($_POST['upsell_ids']) ? array_map('intval', $_POST['upsell_ids']) : [];
|
||||||
if (!empty($upsell_ids) && check_admin_referer('bulk-' . $this->_args['plural'])) {
|
if (!empty($upsell_ids) && check_admin_referer('bulk-' . $this->_args['plural'])) {
|
||||||
foreach ($upsell_ids as $id) {
|
foreach ($upsell_ids as $id) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ $menu_items = [
|
|||||||
'sodino-dashboard' => __('داشبورد', 'sodino'),
|
'sodino-dashboard' => __('داشبورد', 'sodino'),
|
||||||
'sodino-rules' => __('قوانین', 'sodino'),
|
'sodino-rules' => __('قوانین', 'sodino'),
|
||||||
'sodino-add-rule' => __('افزودن قانون', 'sodino'),
|
'sodino-add-rule' => __('افزودن قانون', 'sodino'),
|
||||||
|
'sodino-templates' => __('قالبهای آماده', 'sodino'),
|
||||||
'sodino-upsells' => __('آپسل (پیشنهاد فروش)', 'sodino'),
|
'sodino-upsells' => __('آپسل (پیشنهاد فروش)', 'sodino'),
|
||||||
'sodino-add-upsell' => __('افزودن آپسل', 'sodino'),
|
'sodino-add-upsell' => __('افزودن آپسل', 'sodino'),
|
||||||
'sodino-banners' => __('بنرهای هوشمند', 'sodino'),
|
'sodino-banners' => __('بنرهای هوشمند', 'sodino'),
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ $condition_value = $condition['value'] ?? '';
|
|||||||
<option value="cart_contains_category" <?php selected($condition_type, 'cart_contains_category'); ?>><?php _e('سبد شامل دستهبندی', 'sodino'); ?></option>
|
<option value="cart_contains_category" <?php selected($condition_type, 'cart_contains_category'); ?>><?php _e('سبد شامل دستهبندی', 'sodino'); ?></option>
|
||||||
<option value="customer_order_count_min" <?php selected($condition_type, 'customer_order_count_min'); ?>><?php _e('حداقل سفارش مشتری', 'sodino'); ?></option>
|
<option value="customer_order_count_min" <?php selected($condition_type, 'customer_order_count_min'); ?>><?php _e('حداقل سفارش مشتری', 'sodino'); ?></option>
|
||||||
<option value="customer_order_count_max" <?php selected($condition_type, 'customer_order_count_max'); ?>><?php _e('حداکثر سفارش مشتری', 'sodino'); ?></option>
|
<option value="customer_order_count_max" <?php selected($condition_type, 'customer_order_count_max'); ?>><?php _e('حداکثر سفارش مشتری', 'sodino'); ?></option>
|
||||||
|
<option value="customer_days_since_last_order_min" <?php selected($condition_type, 'customer_days_since_last_order_min'); ?>><?php _e('حداقل روز از آخرین سفارش', 'sodino'); ?></option>
|
||||||
|
<option value="product_total_sales_max" <?php selected($condition_type, 'product_total_sales_max'); ?>><?php _e('حداکثر فروش کل محصول', 'sodino'); ?></option>
|
||||||
|
<option value="product_total_sales_min" <?php selected($condition_type, 'product_total_sales_min'); ?>><?php _e('حداقل فروش کل محصول', 'sodino'); ?></option>
|
||||||
<option value="day_of_week" <?php selected($condition_type, 'day_of_week'); ?>><?php _e('روز هفته', 'sodino'); ?></option>
|
<option value="day_of_week" <?php selected($condition_type, 'day_of_week'); ?>><?php _e('روز هفته', 'sodino'); ?></option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ $weekdays = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($selectedTemplate)) : ?>
|
||||||
|
<div class="mb-6 rounded-lg border border-blue-200 bg-blue-50 p-4 text-sm text-blue-800">
|
||||||
|
<strong><?php echo esc_html(sprintf(__('قالب انتخابشده: %s', 'sodino'), $selectedTemplate['title'])); ?></strong>
|
||||||
|
<p class="mt-1"><?php _e('شرطها و عملیات پیشنهادی آماده شدهاند. قبل از ذخیره، مقدار تخفیف و محدودیتها را با سیاست فروشگاه هماهنگ کنید.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<form method="post" class="space-y-6" id="sodino-rule-form">
|
<form method="post" class="space-y-6" id="sodino-rule-form">
|
||||||
<?php wp_nonce_field('sodino_save_rule', 'sodino_rule_nonce'); ?>
|
<?php wp_nonce_field('sodino_save_rule', 'sodino_rule_nonce'); ?>
|
||||||
|
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-rules');
|
|||||||
<!-- Rules Table -->
|
<!-- Rules Table -->
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
<?php wp_nonce_field('bulk-sodino_rules'); ?>
|
||||||
<?php $rulesTable->display(); ?>
|
<?php $rulesTable->display(); ?>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
80
admin/views/templates.php
Normal file
80
admin/views/templates.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$current_page = 'sodino-templates';
|
||||||
|
require_once SODINO_PLUGIN_DIR . 'admin/components/layout.php';
|
||||||
|
|
||||||
|
sodino_admin_layout($current_page, function() use ($ruleTemplates, $upsellTemplates) {
|
||||||
|
?>
|
||||||
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8">
|
||||||
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-semibold text-gray-900"><?php _e('قالبهای آماده فروش', 'sodino'); ?></h2>
|
||||||
|
<p class="mt-2 text-gray-600"><?php _e('یک سناریوی آماده را انتخاب کنید؛ فرم مربوط با شرطها و عملیات پیشنهادی پر میشود.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="mb-8">
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900"><?php _e('قالبهای قوانین قیمتگذاری', 'sodino'); ?></h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500"><?php _e('بعد از انتخاب قالب، مقدارها را مطابق استراتژی فروشگاه تنظیم و ذخیره کنید.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
|
<?php foreach ($ruleTemplates as $key => $template) : ?>
|
||||||
|
<article class="bg-white rounded-lg border border-gray-200 p-5 shadow-sm">
|
||||||
|
<div class="flex h-full flex-col justify-between gap-5">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-base font-semibold text-gray-900"><?php echo esc_html($template['title']); ?></h4>
|
||||||
|
<p class="mt-2 text-sm leading-6 text-gray-600"><?php echo esc_html($template['description']); ?></p>
|
||||||
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
|
<?php foreach ((array) $template['actions'] as $action) : ?>
|
||||||
|
<span class="rounded-full bg-blue-50 px-3 py-1 text-xs font-medium text-blue-700">
|
||||||
|
<?php echo esc_html(sprintf('%s: %s', $action['type'], $action['value'])); ?>
|
||||||
|
</span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="<?php echo esc_url(admin_url('admin.php?page=sodino-add-rule&template=' . $key)); ?>" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-700">
|
||||||
|
<?php _e('استفاده از قالب', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h3 class="text-lg font-semibold text-gray-900"><?php _e('قالبهای آپسل', 'sodino'); ?></h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500"><?php _e('برای پیشنهاد محصول مکمل، محصول فعالساز و محصول پیشنهادی را در فرم انتخاب کنید.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
|
<?php foreach ($upsellTemplates as $key => $template) : ?>
|
||||||
|
<article class="bg-white rounded-lg border border-gray-200 p-5 shadow-sm">
|
||||||
|
<div class="flex h-full flex-col justify-between gap-5">
|
||||||
|
<div>
|
||||||
|
<h4 class="text-base font-semibold text-gray-900"><?php echo esc_html($template['title']); ?></h4>
|
||||||
|
<p class="mt-2 text-sm leading-6 text-gray-600"><?php echo esc_html($template['description']); ?></p>
|
||||||
|
<div class="mt-4 flex flex-wrap gap-2">
|
||||||
|
<span class="rounded-full bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-700">
|
||||||
|
<?php echo esc_html(sprintf(__('تخفیف پیشفرض: %s%%', 'sodino'), $template['discount_value'])); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="<?php echo esc_url(admin_url('admin.php?page=sodino-add-upsell&template=' . $key)); ?>" class="inline-flex items-center justify-center rounded-lg bg-emerald-600 px-4 py-2 text-sm font-semibold text-white hover:bg-emerald-700">
|
||||||
|
<?php _e('ساخت آپسل از قالب', 'sodino'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<?php
|
||||||
|
});
|
||||||
|
?>
|
||||||
@@ -92,6 +92,13 @@ $product_categories = get_terms([
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if (!empty($selectedTemplate)) : ?>
|
||||||
|
<div class="mb-6 rounded-lg border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">
|
||||||
|
<strong><?php echo esc_html(sprintf(__('قالب انتخابشده: %s', 'sodino'), $selectedTemplate['title'])); ?></strong>
|
||||||
|
<p class="mt-1"><?php _e('ساختار آپسل آماده شده است. محصول فعالساز و محصول پیشنهادی را انتخاب کنید و سپس ذخیره کنید.', 'sodino'); ?></p>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
<form method="post" class="space-y-6">
|
<form method="post" class="space-y-6">
|
||||||
<?php wp_nonce_field('sodino_save_upsell', 'sodino_upsell_nonce'); ?>
|
<?php wp_nonce_field('sodino_save_upsell', 'sodino_upsell_nonce'); ?>
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-upsells');
|
|||||||
|
|
||||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
|
||||||
<form method="post">
|
<form method="post">
|
||||||
|
<?php wp_nonce_field('bulk-sodino_upsells'); ?>
|
||||||
<?php $upsellTable->display(); ?>
|
<?php $upsellTable->display(); ?>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ class AdminController {
|
|||||||
'cart_contains_category',
|
'cart_contains_category',
|
||||||
'customer_order_count_min',
|
'customer_order_count_min',
|
||||||
'customer_order_count_max',
|
'customer_order_count_max',
|
||||||
|
'customer_days_since_last_order_min',
|
||||||
|
'product_total_sales_max',
|
||||||
|
'product_total_sales_min',
|
||||||
'day_of_week',
|
'day_of_week',
|
||||||
];
|
];
|
||||||
private $allowedRuleConditionOperators = ['is', 'is_not', 'in', 'not_in'];
|
private $allowedRuleConditionOperators = ['is', 'is_not', 'in', 'not_in'];
|
||||||
@@ -81,6 +84,15 @@ class AdminController {
|
|||||||
[$this, 'addRulePage']
|
[$this, 'addRulePage']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'sodino-rules',
|
||||||
|
__('قالبهای آماده', 'sodino'),
|
||||||
|
__('قالبهای آماده', 'sodino'),
|
||||||
|
'manage_options',
|
||||||
|
'sodino-templates',
|
||||||
|
[$this, 'templatesPage']
|
||||||
|
);
|
||||||
|
|
||||||
add_submenu_page(
|
add_submenu_page(
|
||||||
'sodino-rules',
|
'sodino-rules',
|
||||||
__('آپسل (پیشنهاد فروش)', 'sodino'),
|
__('آپسل (پیشنهاد فروش)', 'sodino'),
|
||||||
@@ -199,6 +211,12 @@ class AdminController {
|
|||||||
$this->listRulesPage();
|
$this->listRulesPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function templatesPage() {
|
||||||
|
$ruleTemplates = $this->getRuleTemplates();
|
||||||
|
$upsellTemplates = $this->getUpsellTemplates();
|
||||||
|
include SODINO_PLUGIN_DIR . 'admin/views/templates.php';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dashboard page
|
* Dashboard page
|
||||||
*/
|
*/
|
||||||
@@ -249,6 +267,8 @@ class AdminController {
|
|||||||
$this->saveRule();
|
$this->saveRule();
|
||||||
} else {
|
} else {
|
||||||
$rule = new Rule();
|
$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';
|
include SODINO_PLUGIN_DIR . 'admin/views/rule-form.php';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,6 +304,8 @@ class AdminController {
|
|||||||
$this->saveUpsell();
|
$this->saveUpsell();
|
||||||
} else {
|
} else {
|
||||||
$upsell = new Upsell();
|
$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';
|
include SODINO_PLUGIN_DIR . 'admin/views/upsell-form.php';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -371,6 +393,117 @@ class AdminController {
|
|||||||
include SODINO_PLUGIN_DIR . 'admin/views/banner-list.php';
|
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() {
|
private function editBannerPage() {
|
||||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||||
$banner = $this->bannerRepository->getById($id);
|
$banner = $this->bannerRepository->getById($id);
|
||||||
@@ -522,7 +655,10 @@ class AdminController {
|
|||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -531,13 +667,13 @@ class AdminController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_GET['action'] === 'delete_upsell') {
|
if ($action === 'delete_upsell') {
|
||||||
$this->upsellRepository->delete($id);
|
$this->upsellRepository->delete($id);
|
||||||
wp_safe_redirect(admin_url('admin.php?page=sodino-upsells'));
|
wp_safe_redirect(admin_url('admin.php?page=sodino-upsells'));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_GET['action'] === 'toggle_upsell_status') {
|
if ($action === 'toggle_upsell_status') {
|
||||||
$upsell = $this->upsellRepository->getById($id);
|
$upsell = $this->upsellRepository->getById($id);
|
||||||
if ($upsell) {
|
if ($upsell) {
|
||||||
$upsell->status = $upsell->status ? 0 : 1;
|
$upsell->status = $upsell->status ? 0 : 1;
|
||||||
@@ -553,7 +689,10 @@ class AdminController {
|
|||||||
return;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -562,13 +701,13 @@ class AdminController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_GET['action'] === 'delete_banner') {
|
if ($action === 'delete_banner') {
|
||||||
$this->bannerRepository->delete($id);
|
$this->bannerRepository->delete($id);
|
||||||
wp_safe_redirect(admin_url('admin.php?page=sodino-banners'));
|
wp_safe_redirect(admin_url('admin.php?page=sodino-banners'));
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($_GET['action'] === 'toggle_banner_status') {
|
if ($action === 'toggle_banner_status') {
|
||||||
$banner = $this->bannerRepository->getById($id);
|
$banner = $this->bannerRepository->getById($id);
|
||||||
if ($banner) {
|
if ($banner) {
|
||||||
$banner->status = $banner->status ? 0 : 1;
|
$banner->status = $banner->status ? 0 : 1;
|
||||||
@@ -588,7 +727,7 @@ class AdminController {
|
|||||||
wp_send_json([]);
|
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')) {
|
if (empty($term) || !function_exists('wc_get_products')) {
|
||||||
wp_send_json([]);
|
wp_send_json([]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ class Cache {
|
|||||||
* Get cached value
|
* Get cached value
|
||||||
*/
|
*/
|
||||||
public function get($key, $group = 'sodino') {
|
public function get($key, $group = 'sodino') {
|
||||||
|
if (!$this->isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$full_key = $this->buildKey($key, $group);
|
$full_key = $this->buildKey($key, $group);
|
||||||
|
|
||||||
// Check memory cache first
|
// Check memory cache first
|
||||||
@@ -42,6 +46,10 @@ class Cache {
|
|||||||
* Set cached value
|
* Set cached value
|
||||||
*/
|
*/
|
||||||
public function set($key, $value, $expiration = 3600, $group = 'sodino') {
|
public function set($key, $value, $expiration = 3600, $group = 'sodino') {
|
||||||
|
if (!$this->isEnabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$full_key = $this->buildKey($key, $group);
|
$full_key = $this->buildKey($key, $group);
|
||||||
|
|
||||||
// Set in memory cache
|
// Set in memory cache
|
||||||
@@ -134,4 +142,12 @@ class Cache {
|
|||||||
private function buildKey($key, $group) {
|
private function buildKey($key, $group) {
|
||||||
return "sodino_{$group}_{$key}";
|
return "sodino_{$group}_{$key}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function isEnabled() {
|
||||||
|
if (!class_exists(__NAMESPACE__ . '\Settings')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Settings::getInstance()->isCacheEnabled();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,14 +73,12 @@ class BannerRepository {
|
|||||||
|
|
||||||
public function incrementImpression($id) {
|
public function incrementImpression($id) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$wpdb->query($wpdb->prepare("UPDATE {$this->table_name} SET impressions = impressions + 1 WHERE id = %d", $id));
|
return $wpdb->query($wpdb->prepare("UPDATE {$this->table_name} SET impressions = impressions + 1 WHERE id = %d", $id));
|
||||||
$this->clearCache();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function incrementClick($id) {
|
public function incrementClick($id) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$wpdb->query($wpdb->prepare("UPDATE {$this->table_name} SET clicks = clicks + 1 WHERE id = %d", $id));
|
return $wpdb->query($wpdb->prepare("UPDATE {$this->table_name} SET clicks = clicks + 1 WHERE id = %d", $id));
|
||||||
$this->clearCache();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function clearCache() {
|
public function clearCache() {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class EventRepository {
|
|||||||
$where = $this->buildWhereClauses($filters, $params);
|
$where = $this->buildWhereClauses($filters, $params);
|
||||||
|
|
||||||
$sql = "SELECT * FROM {$this->table_name} WHERE " . implode(' AND ', $where) . " ORDER BY created_at ASC";
|
$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 = []) {
|
public function getCount(array $filters = []) {
|
||||||
@@ -32,7 +32,7 @@ class EventRepository {
|
|||||||
$where = $this->buildWhereClauses($filters, $params);
|
$where = $this->buildWhereClauses($filters, $params);
|
||||||
|
|
||||||
$sql = "SELECT COUNT(*) FROM {$this->table_name} WHERE " . implode(' AND ', $where);
|
$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 = []) {
|
public function getSum($field, array $filters = []) {
|
||||||
@@ -45,7 +45,7 @@ class EventRepository {
|
|||||||
$where = $this->buildWhereClauses($filters, $params);
|
$where = $this->buildWhereClauses($filters, $params);
|
||||||
|
|
||||||
$sql = "SELECT SUM({$field}) FROM {$this->table_name} WHERE " . implode(' AND ', $where);
|
$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) {
|
public function getRuleUsageCount($rule_id) {
|
||||||
@@ -95,4 +95,14 @@ class EventRepository {
|
|||||||
|
|
||||||
return $where;
|
return $where;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function prepareSql($sql, array $params) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if (empty($params)) {
|
||||||
|
return $sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wpdb->prepare($sql, $params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,12 +126,18 @@ class RuleRepository {
|
|||||||
*/
|
*/
|
||||||
public function incrementUsage($id) {
|
public function incrementUsage($id) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
return $wpdb->query(
|
$result = $wpdb->query(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
"UPDATE {$this->table_name} SET usage_count = usage_count + 1 WHERE id = %d",
|
"UPDATE {$this->table_name} SET usage_count = usage_count + 1 WHERE id = %d",
|
||||||
$id
|
$id
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($result !== false) {
|
||||||
|
$this->clearCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -44,13 +44,19 @@ class AnalyticsService {
|
|||||||
$summary = $this->getSummary($filters);
|
$summary = $this->getSummary($filters);
|
||||||
$salesChart = $this->getSalesChart($filters);
|
$salesChart = $this->getSalesChart($filters);
|
||||||
$rulePerformance = $this->getRulePerformance($filters);
|
$rulePerformance = $this->getRulePerformance($filters);
|
||||||
|
$roiReport = $this->getRoiReport($filters);
|
||||||
|
$upsellPerformance = $this->getUpsellPerformance($filters);
|
||||||
|
$bannerPerformance = $this->getBannerPerformance($filters, $summary);
|
||||||
$userBehavior = $this->getUserBehavior($filters);
|
$userBehavior = $this->getUserBehavior($filters);
|
||||||
$insights = $this->getInsights($summary, $filters);
|
$insights = $this->getInsights($summary, $filters, $roiReport);
|
||||||
|
|
||||||
$result = [
|
$result = [
|
||||||
'summary' => $summary,
|
'summary' => $summary,
|
||||||
'sales_chart' => $salesChart,
|
'sales_chart' => $salesChart,
|
||||||
'rule_performance' => $rulePerformance,
|
'rule_performance' => $rulePerformance,
|
||||||
|
'roi_report' => $roiReport,
|
||||||
|
'upsell_performance' => $upsellPerformance,
|
||||||
|
'banner_performance' => $bannerPerformance,
|
||||||
'user_behavior' => $userBehavior,
|
'user_behavior' => $userBehavior,
|
||||||
'insights' => $insights,
|
'insights' => $insights,
|
||||||
];
|
];
|
||||||
@@ -152,6 +158,160 @@ class AnalyticsService {
|
|||||||
return $result;
|
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 = []) {
|
public function getUserBehavior(array $filters = []) {
|
||||||
$productViewCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'product_view']));
|
$productViewCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'product_view']));
|
||||||
$addToCartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'add_to_cart']));
|
$addToCartCount = $this->eventRepository->getCount(array_merge($filters, ['event_type' => 'add_to_cart']));
|
||||||
@@ -179,9 +339,16 @@ class AnalyticsService {
|
|||||||
return $performance[0]['name'] ?? null;
|
return $performance[0]['name'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getInsights(array $summary, array $filters = []) {
|
private function getInsights(array $summary, array $filters = [], array $roiReport = []) {
|
||||||
$insights = [];
|
$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'])) {
|
if (!empty($summary['best_rule'])) {
|
||||||
$insights[] = sprintf('%s %s', __('قانون برتر:', 'sodino'), esc_html($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) {
|
private function getDateRange($range, $start, $end) {
|
||||||
|
$today = current_time('Y-m-d');
|
||||||
$result = [
|
$result = [
|
||||||
'start' => date('Y-m-d', strtotime('-6 days')),
|
'start' => date('Y-m-d', strtotime($today . ' -6 days')),
|
||||||
'end' => date('Y-m-d'),
|
'end' => $today,
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($range === '30d') {
|
if ($range === '30d') {
|
||||||
$result['start'] = date('Y-m-d', strtotime('-29 days'));
|
$result['start'] = date('Y-m-d', strtotime($today . ' -29 days'));
|
||||||
$result['end'] = date('Y-m-d');
|
$result['end'] = $today;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($range === 'custom' && !empty($start) && !empty($end)) {
|
if ($range === 'custom' && !empty($start) && !empty($end)) {
|
||||||
$result['start'] = date('Y-m-d', strtotime($start));
|
$startTimestamp = strtotime($start);
|
||||||
$result['end'] = date('Y-m-d', strtotime($end));
|
$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;
|
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) {
|
public function getProductIdsByCategory($category_id) {
|
||||||
$products = get_posts([
|
$products = get_posts([
|
||||||
'post_type' => 'product',
|
'post_type' => 'product',
|
||||||
|
|||||||
@@ -112,6 +112,12 @@ class BannerService {
|
|||||||
wp_cache_set('version', $version, 'sodino_banners');
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ class PricingService {
|
|||||||
private $trackingService;
|
private $trackingService;
|
||||||
private $settings;
|
private $settings;
|
||||||
private $cache;
|
private $cache;
|
||||||
private $trackedApplications = [];
|
private $appliedRules = [];
|
||||||
|
private $trackedConversions = [];
|
||||||
|
|
||||||
public function __construct(RuleRepository $ruleRepository, TrackingService $trackingService) {
|
public function __construct(RuleRepository $ruleRepository, TrackingService $trackingService) {
|
||||||
$this->ruleRepository = $ruleRepository;
|
$this->ruleRepository = $ruleRepository;
|
||||||
@@ -29,7 +30,14 @@ class PricingService {
|
|||||||
return $price;
|
return $price;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$product || !is_a($product, 'WC_Product')) {
|
||||||
|
return $price;
|
||||||
|
}
|
||||||
|
|
||||||
$price = $this->normalizePrice($price);
|
$price = $this->normalizePrice($price);
|
||||||
|
if ($price <= 0) {
|
||||||
|
return $price;
|
||||||
|
}
|
||||||
|
|
||||||
if (!$this->settings->get('cart_pricing_enabled') && is_cart()) {
|
if (!$this->settings->get('cart_pricing_enabled') && is_cart()) {
|
||||||
return $price;
|
return $price;
|
||||||
@@ -50,8 +58,7 @@ class PricingService {
|
|||||||
foreach ($rules as $rule) {
|
foreach ($rules as $rule) {
|
||||||
$oldPrice = $price;
|
$oldPrice = $price;
|
||||||
$price = $this->applyRuleActions($rule, $price);
|
$price = $this->applyRuleActions($rule, $price);
|
||||||
|
$this->rememberAppliedRule($product, $oldPrice, $price, $rule->id);
|
||||||
$this->trackDiscountOnce($product, $oldPrice, $price, $rule->id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$price = $this->enforceLimits($originalPrice, $price);
|
$price = $this->enforceLimits($originalPrice, $price);
|
||||||
@@ -59,6 +66,56 @@ class PricingService {
|
|||||||
return max(0, $price);
|
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) {
|
private function getApplicableRules($product) {
|
||||||
$rules = $this->ruleRepository->getEnabled();
|
$rules = $this->ruleRepository->getEnabled();
|
||||||
$applicable = [];
|
$applicable = [];
|
||||||
@@ -160,6 +217,12 @@ class PricingService {
|
|||||||
return $this->getCustomerOrderCount() >= intval($value);
|
return $this->getCustomerOrderCount() >= intval($value);
|
||||||
case 'customer_order_count_max':
|
case 'customer_order_count_max':
|
||||||
return $this->getCustomerOrderCount() <= intval($value);
|
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':
|
case 'day_of_week':
|
||||||
return in_array((string) current_time('N'), array_map('strval', $this->normalizeIdList($value)), true);
|
return in_array((string) current_time('N'), array_map('strval', $this->normalizeIdList($value)), true);
|
||||||
default:
|
default:
|
||||||
@@ -195,6 +258,35 @@ class PricingService {
|
|||||||
return (int) wc_get_customer_order_count(get_current_user_id());
|
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) {
|
private function userHasAllowedRole($roles) {
|
||||||
if (!is_user_logged_in()) {
|
if (!is_user_logged_in()) {
|
||||||
return false;
|
return false;
|
||||||
@@ -227,12 +319,36 @@ class PricingService {
|
|||||||
return $this->productHasTerm($product, $categories, 'product_cat');
|
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) {
|
private function productHasTerm($product, $terms, $taxonomy) {
|
||||||
if (!$product || empty($terms)) {
|
if (!$product || empty($terms)) {
|
||||||
return false;
|
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)) {
|
if (is_wp_error($product_terms)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -428,16 +544,30 @@ class PricingService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function trackDiscountOnce($product, $oldPrice, $price, $ruleId) {
|
private function rememberAppliedRule($product, $oldPrice, $price, $ruleId) {
|
||||||
$productId = $product ? $product->get_id() : 0;
|
if ($price >= $oldPrice || !$product || !is_a($product, 'WC_Product')) {
|
||||||
$key = implode(':', [$productId, (int) $ruleId, round($oldPrice, 4), round($price, 4)]);
|
|
||||||
|
|
||||||
if ($price >= $oldPrice || isset($this->trackedApplications[$key])) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->trackedApplications[$key] = true;
|
foreach ($this->getProductTrackingKeys($product) as $key) {
|
||||||
$this->trackingService->recordDiscountApplied($product, $oldPrice, $price, $ruleId);
|
$this->appliedRules[$key][(int) $ruleId] = [
|
||||||
$this->ruleRepository->incrementUsage($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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,6 +98,10 @@ class TrackingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function logEvent($type, array $data = []) {
|
private function logEvent($type, array $data = []) {
|
||||||
|
if (empty($type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$event = [
|
$event = [
|
||||||
'event_type' => $type,
|
'event_type' => $type,
|
||||||
'product_id' => isset($data['product_id']) ? intval($data['product_id']) : null,
|
'product_id' => isset($data['product_id']) ? intval($data['product_id']) : null,
|
||||||
@@ -111,7 +115,7 @@ class TrackingService {
|
|||||||
'created_at' => current_time('mysql'),
|
'created_at' => current_time('mysql'),
|
||||||
];
|
];
|
||||||
|
|
||||||
$this->eventRepository->insert($event);
|
return $this->eventRepository->insert($event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getSessionId() {
|
private function getSessionId() {
|
||||||
@@ -124,7 +128,10 @@ class TrackingService {
|
|||||||
return $session_id;
|
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) {
|
private function hasLogged($key) {
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"authors": [
|
"authors": [
|
||||||
{
|
{
|
||||||
"name": "Your Name",
|
"name": "Soheil Khaledabadi"
|
||||||
"email": "your.email@example.com"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ add_action('woocommerce_before_cart', 'sodino_render_cart_banner');
|
|||||||
add_action('wp_enqueue_scripts', 'sodino_enqueue_banner_assets');
|
add_action('wp_enqueue_scripts', 'sodino_enqueue_banner_assets');
|
||||||
add_action('wp_ajax_nopriv_sodino_banner_click', 'sodino_handle_banner_click');
|
add_action('wp_ajax_nopriv_sodino_banner_click', 'sodino_handle_banner_click');
|
||||||
add_action('wp_ajax_sodino_banner_click', 'sodino_handle_banner_click');
|
add_action('wp_ajax_sodino_banner_click', 'sodino_handle_banner_click');
|
||||||
|
add_action('woocommerce_checkout_create_order', 'sodino_add_banner_order_meta', 20, 2);
|
||||||
|
|
||||||
function sodino_enqueue_banner_assets() {
|
function sodino_enqueue_banner_assets() {
|
||||||
if (is_admin()) {
|
if (is_admin()) {
|
||||||
@@ -164,12 +165,15 @@ function sodino_render_cart_banner() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sodino_handle_banner_click() {
|
function sodino_handle_banner_click() {
|
||||||
$nonce = $_POST['nonce'] ?? ($_POST['security'] ?? '');
|
$nonce = isset($_POST['nonce'])
|
||||||
|
? sanitize_text_field(wp_unslash($_POST['nonce']))
|
||||||
|
: (isset($_POST['security']) ? sanitize_text_field(wp_unslash($_POST['security'])) : '');
|
||||||
|
|
||||||
if (!isset($_POST['banner_id']) || !wp_verify_nonce($nonce, 'sodino_banner_click')) {
|
if (!isset($_POST['banner_id']) || !wp_verify_nonce($nonce, 'sodino_banner_click')) {
|
||||||
wp_send_json_error();
|
wp_send_json_error();
|
||||||
}
|
}
|
||||||
|
|
||||||
$bannerId = intval($_POST['banner_id']);
|
$bannerId = absint(wp_unslash($_POST['banner_id']));
|
||||||
if (!$bannerId) {
|
if (!$bannerId) {
|
||||||
wp_send_json_error();
|
wp_send_json_error();
|
||||||
}
|
}
|
||||||
@@ -180,5 +184,46 @@ function sodino_handle_banner_click() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$sodino_banner_service->increaseClick($bannerId);
|
$sodino_banner_service->increaseClick($bannerId);
|
||||||
|
sodino_remember_banner_click($bannerId);
|
||||||
wp_send_json_success();
|
wp_send_json_success();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sodino_remember_banner_click($bannerId) {
|
||||||
|
if (!function_exists('WC') || !WC()->session) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$clicks = WC()->session->get('sodino_banner_clicks', []);
|
||||||
|
if (!is_array($clicks)) {
|
||||||
|
$clicks = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$clicks[(int) $bannerId] = current_time('timestamp');
|
||||||
|
WC()->session->set('sodino_banner_clicks', $clicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sodino_add_banner_order_meta($order, $data) {
|
||||||
|
if (!function_exists('WC') || !WC()->session || !$order) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$clicks = WC()->session->get('sodino_banner_clicks', []);
|
||||||
|
if (empty($clicks) || !is_array($clicks)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validClicks = [];
|
||||||
|
$cutoff = current_time('timestamp') - DAY_IN_SECONDS;
|
||||||
|
foreach ($clicks as $bannerId => $timestamp) {
|
||||||
|
if ((int) $bannerId > 0 && (int) $timestamp >= $cutoff) {
|
||||||
|
$validClicks[] = (int) $bannerId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($validClicks)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order->update_meta_data('_sodino_banner_click_ids', implode(',', array_values(array_unique($validClicks))));
|
||||||
|
WC()->session->__unset('sodino_banner_clicks');
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,11 @@ add_filter('woocommerce_product_get_sale_price', 'sodino_apply_dynamic_pricing',
|
|||||||
add_filter('woocommerce_product_variation_get_price', 'sodino_apply_dynamic_pricing', 10, 2);
|
add_filter('woocommerce_product_variation_get_price', 'sodino_apply_dynamic_pricing', 10, 2);
|
||||||
add_filter('woocommerce_product_variation_get_sale_price', 'sodino_apply_dynamic_pricing', 10, 2);
|
add_filter('woocommerce_product_variation_get_sale_price', 'sodino_apply_dynamic_pricing', 10, 2);
|
||||||
add_filter('woocommerce_package_rates', 'sodino_apply_free_shipping_rules', 20, 1);
|
add_filter('woocommerce_package_rates', 'sodino_apply_free_shipping_rules', 20, 1);
|
||||||
|
add_filter('woocommerce_add_cart_item_data', 'sodino_add_dynamic_pricing_cart_item_data', 20, 4);
|
||||||
|
add_filter('woocommerce_get_cart_item_from_session', 'sodino_restore_dynamic_pricing_cart_item_data', 20, 2);
|
||||||
|
add_filter('woocommerce_get_item_data', 'sodino_display_dynamic_pricing_cart_item_data', 20, 2);
|
||||||
|
add_action('woocommerce_checkout_create_order_line_item', 'sodino_add_dynamic_pricing_order_item_meta', 20, 4);
|
||||||
|
add_action('woocommerce_add_to_cart', 'sodino_track_dynamic_pricing_conversion', 30, 6);
|
||||||
|
|
||||||
function sodino_apply_dynamic_pricing($price, $product) {
|
function sodino_apply_dynamic_pricing($price, $product) {
|
||||||
global $sodino_pricing_service;
|
global $sodino_pricing_service;
|
||||||
@@ -32,3 +37,95 @@ function sodino_apply_free_shipping_rules($rates) {
|
|||||||
global $sodino_pricing_service;
|
global $sodino_pricing_service;
|
||||||
return $sodino_pricing_service->applyFreeShippingRates($rates);
|
return $sodino_pricing_service->applyFreeShippingRates($rates);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sodino_add_dynamic_pricing_cart_item_data($cart_item_data, $product_id, $variation_id, $quantity) {
|
||||||
|
global $sodino_pricing_service;
|
||||||
|
|
||||||
|
if (!$sodino_pricing_service) {
|
||||||
|
return $cart_item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = wc_get_product($variation_id ?: $product_id);
|
||||||
|
if (!$product) {
|
||||||
|
return $cart_item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$basePrice = $product->get_price('edit');
|
||||||
|
if ($basePrice === '' || !is_numeric($basePrice)) {
|
||||||
|
return $cart_item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$discountedPrice = $sodino_pricing_service->applyDynamicPricing($basePrice, $product);
|
||||||
|
$rules = $sodino_pricing_service->getAppliedRulesForProduct($product);
|
||||||
|
if (empty($rules) || $discountedPrice >= (float) $basePrice) {
|
||||||
|
return $cart_item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cart_item_data['sodino_dynamic_pricing'] = [
|
||||||
|
'rule_ids' => array_values(array_unique(array_map('intval', array_column($rules, 'rule_id')))),
|
||||||
|
'original_price' => (float) $basePrice,
|
||||||
|
'discounted_price' => (float) $discountedPrice,
|
||||||
|
'discount_value' => max(0, (float) $basePrice - (float) $discountedPrice),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $cart_item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sodino_restore_dynamic_pricing_cart_item_data($cart_item, $values) {
|
||||||
|
if (!empty($values['sodino_dynamic_pricing'])) {
|
||||||
|
$cart_item['sodino_dynamic_pricing'] = $values['sodino_dynamic_pricing'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cart_item;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sodino_display_dynamic_pricing_cart_item_data($item_data, $cart_item) {
|
||||||
|
if (empty($cart_item['sodino_dynamic_pricing']['discount_value'])) {
|
||||||
|
return $item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item_data[] = [
|
||||||
|
'key' => __('تخفیف سودینو', 'sodino'),
|
||||||
|
'value' => wp_kses_post(wc_price((float) $cart_item['sodino_dynamic_pricing']['discount_value'])),
|
||||||
|
];
|
||||||
|
|
||||||
|
return $item_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sodino_add_dynamic_pricing_order_item_meta($item, $cart_item_key, $values, $order) {
|
||||||
|
if (empty($values['sodino_dynamic_pricing'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pricing = $values['sodino_dynamic_pricing'];
|
||||||
|
$ruleIds = !empty($pricing['rule_ids']) ? array_map('intval', (array) $pricing['rule_ids']) : [];
|
||||||
|
|
||||||
|
if (empty($ruleIds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$item->add_meta_data('_sodino_rule_ids', implode(',', $ruleIds), true);
|
||||||
|
$item->add_meta_data('_sodino_rule_discount', (float) ($pricing['discount_value'] ?? 0), true);
|
||||||
|
$item->add_meta_data(__('تخفیف قوانین سودینو', 'sodino'), wc_price((float) ($pricing['discount_value'] ?? 0)), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sodino_track_dynamic_pricing_conversion($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data) {
|
||||||
|
global $sodino_pricing_service;
|
||||||
|
|
||||||
|
if (!$sodino_pricing_service) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$product = wc_get_product($variation_id ?: $product_id);
|
||||||
|
if (!$product) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$basePrice = $product->get_price('edit');
|
||||||
|
if ($basePrice === '' || !is_numeric($basePrice)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sodino_pricing_service->applyDynamicPricing($basePrice, $product);
|
||||||
|
$sodino_pricing_service->trackAppliedRulesForProduct($product);
|
||||||
|
}
|
||||||
|
|||||||
16
readme.txt
16
readme.txt
@@ -2,9 +2,9 @@
|
|||||||
Contributors: Soheil khaledabadi
|
Contributors: Soheil khaledabadi
|
||||||
Tags: woocommerce, pricing, dynamic pricing, revenue optimization
|
Tags: woocommerce, pricing, dynamic pricing, revenue optimization
|
||||||
Requires at least: 5.0
|
Requires at least: 5.0
|
||||||
Tested up to: 6.0
|
Tested up to: 6.9
|
||||||
Requires PHP: 7.4
|
Requires PHP: 7.4
|
||||||
Stable tag: 1.0.0
|
Stable tag: 2.0.0
|
||||||
License: GPLv2 or later
|
License: GPLv2 or later
|
||||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
|
|
||||||
@@ -16,8 +16,10 @@ Sodino dynamically adjusts WooCommerce product prices based on user behavior and
|
|||||||
|
|
||||||
Features:
|
Features:
|
||||||
- Dynamic pricing based on user type (new vs returning)
|
- Dynamic pricing based on user type (new vs returning)
|
||||||
- Rule-based system for discounts
|
- Advanced rule builder for product, category, cart, customer, role, and schedule conditions
|
||||||
- Admin panel to manage rules
|
- Smart upsell offers with real cart discounts and performance tracking
|
||||||
|
- Targeted banners with scheduling, device targeting, and click/impression reporting
|
||||||
|
- Admin dashboard for revenue, discount, conversion, and rule performance
|
||||||
- MVC architecture for extensibility
|
- MVC architecture for extensibility
|
||||||
|
|
||||||
== Installation ==
|
== Installation ==
|
||||||
@@ -34,5 +36,9 @@ Yes, it applies to all product types.
|
|||||||
|
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 2.0.0 =
|
||||||
|
* Added advanced pricing rules, upsells, smart banners, analytics dashboard, and tools page.
|
||||||
|
* Improved database migrations, cache handling, security checks, and WooCommerce compatibility.
|
||||||
|
|
||||||
= 1.0.0 =
|
= 1.0.0 =
|
||||||
* Initial release.
|
* Initial release.
|
||||||
|
|||||||
14
sodino.php
14
sodino.php
@@ -1,17 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Plugin Name: Sodino (سودینو)
|
* Plugin Name: Sodino (سودینو)
|
||||||
* Plugin URI: https://example.com/sodino
|
* Plugin URI: https://sodino.com
|
||||||
* Description: افزونه هوشمند قیمتگذاری و بهینهسازی درآمد برای ووکامرس. قیمت محصولات را بر اساس رفتار کاربر و قوانین تعریفشده به صورت پویا تنظیم میکند.
|
* Description: افزونه هوشمند قیمتگذاری و بهینهسازی درآمد برای ووکامرس. قیمت محصولات را بر اساس رفتار کاربر و قوانین تعریفشده به صورت پویا تنظیم میکند.
|
||||||
* Version: 2.0.0
|
* Version: 2.0.0
|
||||||
* Author: Your Name
|
* Author: Soheil Khaledabadi
|
||||||
* License: GPL v2 or later
|
* License: GPL v2 or later
|
||||||
* Text Domain: sodino
|
* Text Domain: sodino
|
||||||
* Requires at least: 5.0
|
* Requires at least: 5.0
|
||||||
* Tested up to: 6.0
|
* Tested up to: 6.9
|
||||||
* Requires PHP: 7.4
|
* Requires PHP: 7.4
|
||||||
* WC requires at least: 5.0
|
* WC requires at least: 5.0
|
||||||
* WC tested up to: 6.0
|
* WC tested up to: 10.7
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Prevent direct access
|
// Prevent direct access
|
||||||
@@ -134,6 +134,10 @@ add_action('plugins_loaded', 'sodino_init');
|
|||||||
*/
|
*/
|
||||||
function sodino_init_public_hooks() {
|
function sodino_init_public_hooks() {
|
||||||
$settings = \Sodino\Core\Settings::getInstance();
|
$settings = \Sodino\Core\Settings::getInstance();
|
||||||
|
|
||||||
|
if (!$settings->isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if ($settings->isPricingEnabled()) {
|
if ($settings->isPricingEnabled()) {
|
||||||
require_once SODINO_PLUGIN_DIR . 'public/hooks/pricing-hooks.php';
|
require_once SODINO_PLUGIN_DIR . 'public/hooks/pricing-hooks.php';
|
||||||
@@ -147,7 +151,7 @@ function sodino_init_public_hooks() {
|
|||||||
require_once SODINO_PLUGIN_DIR . 'public/hooks/banner-hooks.php';
|
require_once SODINO_PLUGIN_DIR . 'public/hooks/banner-hooks.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always load analytics
|
// Load analytics while Sodino is enabled.
|
||||||
require_once SODINO_PLUGIN_DIR . 'public/hooks/analytics-hooks.php';
|
require_once SODINO_PLUGIN_DIR . 'public/hooks/analytics-hooks.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user