refactor(Core): refactor and optimize code

This commit is contained in:
2026-05-06 00:54:24 +03:30
parent 32c065e4b6
commit dec4e67b9e
20 changed files with 1787 additions and 361 deletions

71
.gitignore vendored Normal file
View File

@@ -0,0 +1,71 @@
# WordPress
.htaccess
wp-config.php
wp-content/uploads/
wp-content/blogs.dir/
wp-content/upgrade/
wp-content/backup-db/
wp-content/advanced-cache.php
wp-content/wp-cache-config.php
wp-content/cache/
wp-content/cache/supercache/
# WP-CLI
wp-cli.local.yml
# Node
node_modules/
npm-debug.log
yarn-error.log
package-lock.json
yarn.lock
# Build
/dist/
/build/
*.min.js
*.min.css
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store
# Composer
/vendor/
composer.lock
# Logs
*.log
error_log
debug.log
# Temporary files
*.tmp
*.temp
*.cache
# OS
Thumbs.db
.DS_Store
# Backup files
*.bak
*.backup
*~
# Environment
.env
.env.local
.env.*.local
# Testing
/tests/coverage/
.phpunit.result.cache
# Deployment
deploy.sh
.deployment

View File

@@ -4,60 +4,196 @@ if (!defined('ABSPATH')) {
exit; exit;
} }
use Sodino\Controllers\RuleController;
use Sodino\Controllers\DashboardController;
use Sodino\Controllers\SettingsController;
use Sodino\Controllers\AdminController; use Sodino\Controllers\AdminController;
use Sodino\Repositories\BannerRepository; use Sodino\Repositories\BannerRepository;
use Sodino\Repositories\RuleRepository; use Sodino\Repositories\RuleRepository;
use Sodino\Repositories\UpsellRepository; use Sodino\Repositories\UpsellRepository;
use Sodino\Repositories\EventRepository;
// Initialize admin // Initialize repositories
$ruleRepository = new RuleRepository(); $ruleRepository = new RuleRepository();
$upsellRepository = new UpsellRepository(); $upsellRepository = new UpsellRepository();
$bannerRepository = new BannerRepository(); $bannerRepository = new BannerRepository();
$eventRepository = new EventRepository();
// Initialize controllers
$ruleController = new RuleController($ruleRepository);
$dashboardController = new DashboardController($eventRepository, $ruleRepository);
$settingsController = new SettingsController();
$adminController = new AdminController($ruleRepository, $upsellRepository, $bannerRepository); $adminController = new AdminController($ruleRepository, $upsellRepository, $bannerRepository);
// Add menu /**
add_action('admin_menu', [$adminController, 'addMenu']); * Add admin menu
*/
add_action('admin_menu', function() use ($adminController) {
add_menu_page(
__('سودینو', 'sodino'),
__('سودینو', 'sodino'),
'manage_options',
'sodino-dashboard',
[$adminController, 'dashboardPage'],
'dashicons-money-alt',
56
);
// Admin AJAX handlers add_submenu_page(
'sodino-dashboard',
__('داشبورد', 'sodino'),
__('داشبورد', 'sodino'),
'manage_options',
'sodino-dashboard',
[$adminController, 'dashboardPage']
);
add_submenu_page(
'sodino-dashboard',
__('قوانین قیمت‌گذاری', 'sodino'),
__('قوانین قیمت‌گذاری', 'sodino'),
'manage_options',
'sodino-rules',
[$adminController, 'rulesPage']
);
add_submenu_page(
'sodino-dashboard',
__('افزودن قانون', 'sodino'),
__('افزودن قانون', 'sodino'),
'manage_options',
'sodino-add-rule',
[$adminController, 'addRulePage']
);
add_submenu_page(
'sodino-dashboard',
__('آپسل (پیشنهاد فروش)', 'sodino'),
__('آپسل (پیشنهاد فروش)', 'sodino'),
'manage_options',
'sodino-upsells',
[$adminController, 'upsellsPage']
);
add_submenu_page(
'sodino-dashboard',
__('افزودن آپسل', 'sodino'),
__('افزودن آپسل', 'sodino'),
'manage_options',
'sodino-add-upsell',
[$adminController, 'addUpsellPage']
);
add_submenu_page(
'sodino-dashboard',
__('بنرهای هوشمند', 'sodino'),
__('بنرهای هوشمند', 'sodino'),
'manage_options',
'sodino-banners',
[$adminController, 'bannersPage']
);
add_submenu_page(
'sodino-dashboard',
__('افزودن بنر', 'sodino'),
__('افزودن بنر', 'sodino'),
'manage_options',
'sodino-add-banner',
[$adminController, 'addBannerPage']
);
add_submenu_page(
'sodino-dashboard',
__('تنظیمات', 'sodino'),
__('تنظیمات', 'sodino'),
'manage_options',
'sodino-settings',
[$adminController, 'settingsPage']
);
});
/**
* Admin AJAX handlers
*/
add_action('wp_ajax_sodino_search_products', [$adminController, 'searchProductsAjax']); add_action('wp_ajax_sodino_search_products', [$adminController, 'searchProductsAjax']);
// Enqueue admin assets /**
add_action('admin_enqueue_scripts', function($hook) use ($adminController) { * Enqueue admin assets
*/
add_action('admin_enqueue_scripts', function($hook) {
if (strpos($hook, 'sodino') === false) { if (strpos($hook, 'sodino') === false) {
return; return;
} }
// Enqueue Tailwind via CDN script // Enqueue Tailwind via CDN
wp_enqueue_script('sodino-tailwind', 'https://cdn.tailwindcss.com', [], null); wp_enqueue_script('sodino-tailwind', 'https://cdn.tailwindcss.com', [], SODINO_VERSION);
// Admin CSS
wp_enqueue_style('sodino-admin', plugin_dir_url(__FILE__) . 'css/admin.css', [], SODINO_VERSION); wp_enqueue_style('sodino-admin', plugin_dir_url(__FILE__) . 'css/admin.css', [], SODINO_VERSION);
if (strpos($hook, 'sodino_page_sodino-dashboard') !== false) { // Dashboard specific scripts
if (strpos($hook, 'sodino-dashboard') !== false || strpos($hook, 'sodino_page_sodino-dashboard') !== false) {
wp_enqueue_script('sodino-chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true); wp_enqueue_script('sodino-chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', [], null, true);
wp_enqueue_script('sodino-dashboard-js', plugin_dir_url(__FILE__) . 'js/dashboard.js', ['sodino-chart-js'], null, true); wp_enqueue_script('sodino-dashboard-js', plugin_dir_url(__FILE__) . 'js/dashboard.js', ['sodino-chart-js'], SODINO_VERSION, true);
} }
if (strpos($hook, 'sodino_page_sodino-add-upsell') !== false) { // Upsell specific scripts
wp_enqueue_script('sodino-upsell-admin', plugin_dir_url(__FILE__) . 'js/upsell-admin.js', [], SODINO_VERSION, true); if (strpos($hook, 'sodino-add-upsell') !== false || strpos($hook, 'sodino_page_sodino-add-upsell') !== false) {
wp_enqueue_script('sodino-upsell-admin', plugin_dir_url(__FILE__) . 'js/upsell-admin.js', ['jquery'], SODINO_VERSION, true);
wp_localize_script('sodino-upsell-admin', 'sodinoUpsellAdmin', [ wp_localize_script('sodino-upsell-admin', 'sodinoUpsellAdmin', [
'nonce' => wp_create_nonce('sodino_search_products'), 'nonce' => wp_create_nonce('sodino_search_products'),
'ajaxUrl' => admin_url('admin-ajax.php')
]); ]);
} }
if (strpos($hook, 'sodino_page_sodino-add-banner') !== false) { // Banner specific scripts
if (strpos($hook, 'sodino-add-banner') !== false || strpos($hook, 'sodino_page_sodino-add-banner') !== false) {
wp_enqueue_media(); wp_enqueue_media();
wp_enqueue_script('sodino-banner-admin', plugin_dir_url(__FILE__) . 'js/banner-admin.js', ['jquery'], SODINO_VERSION, true); wp_enqueue_script('sodino-banner-admin', plugin_dir_url(__FILE__) . 'js/banner-admin.js', ['jquery'], SODINO_VERSION, true);
} }
}); });
// Handle delete for any Sodino admin page /**
if (isset($_GET['page']) && strpos($_GET['page'], 'sodino') === 0 && isset($_GET['action']) && $_GET['action'] === 'delete') { * Handle admin actions
add_action('admin_init', [$adminController, 'handleDelete']); */
} add_action('admin_init', function() use ($ruleController, $settingsController, $adminController) {
if (isset($_GET['page']) && strpos($_GET['page'], 'sodino') === 0 && isset($_GET['action']) && in_array($_GET['action'], ['delete_banner', 'toggle_banner_status'], true)) { $page = $_GET['page'] ?? '';
add_action('admin_init', [$adminController, 'handleBannerActions']); $action = $_GET['action'] ?? '';
}
// Handle upsell actions // Rule actions
if (isset($_GET['page']) && strpos($_GET['page'], 'sodino') === 0 && isset($_GET['action']) && in_array($_GET['action'], ['delete_upsell', 'toggle_upsell_status'], true)) { if ($page === 'sodino-rules' && $action === 'delete') {
add_action('admin_init', [$adminController, 'handleUpsellActions']); $ruleController->delete();
} }
// Settings actions
if ($page === 'sodino-settings' && $action === 'clear_cache') {
$settingsController->clearCache();
}
// Banner actions
if (strpos($page, 'sodino') === 0 && in_array($action, ['delete_banner', 'toggle_banner_status'], true)) {
$adminController->handleBannerActions();
}
// Upsell actions
if (strpos($page, 'sodino') === 0 && in_array($action, ['delete_upsell', 'toggle_upsell_status'], true)) {
$adminController->handleUpsellActions();
}
});
/**
* Show admin notices
*/
add_action('admin_notices', function() {
$notice = get_transient('sodino_admin_notice');
if ($notice) {
$class = $notice['type'] === 'error' ? 'notice-error' : 'notice-success';
printf(
'<div class="notice %s is-dismissible"><p>%s</p></div>',
esc_attr($class),
esc_html($notice['message'])
);
delete_transient('sodino_admin_notice');
}
});

View File

@@ -0,0 +1,18 @@
<?php
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="bg-white border-b border-gray-200">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="py-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-3xl font-bold text-gray-900"><?php _e('سودینو', 'sodino'); ?></h1>
<p class="mt-1 text-sm text-gray-500"><?php _e('بهینه‌سازی هوشمند فروش', 'sodino'); ?></p>
</div>
</div>
</div>
</div>
</div>

181
admin/components/layout.php Normal file
View File

@@ -0,0 +1,181 @@
<?php
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
/**
* Admin Layout Component
* Usage: sodino_admin_layout($current_page, $content_callback)
*/
function sodino_admin_layout($current_page, $content_callback) {
?>
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
<?php include SODINO_PLUGIN_DIR . 'admin/components/header.php'; ?>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex gap-8">
<?php include SODINO_PLUGIN_DIR . 'admin/components/sidebar.php'; ?>
<main class="flex-1 min-w-0">
<?php call_user_func($content_callback); ?>
</main>
</div>
</div>
</div>
<?php
}
/**
* Card Component
*/
function sodino_card($title = '', $description = '', $content_callback = null, $classes = '') {
?>
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 <?php echo esc_attr($classes); ?>">
<?php if ($title): ?>
<h2 class="text-xl font-semibold text-gray-900 mb-2"><?php echo esc_html($title); ?></h2>
<?php endif; ?>
<?php if ($description): ?>
<p class="text-gray-600 mb-4"><?php echo esc_html($description); ?></p>
<?php endif; ?>
<?php if ($content_callback): ?>
<?php call_user_func($content_callback); ?>
<?php endif; ?>
</div>
<?php
}
/**
* Stats Card Component
*/
function sodino_stat_card($title, $value, $type = 'default') {
$classes = [
'primary' => 'bg-gradient-to-br from-blue-600 to-blue-700 text-white',
'default' => 'bg-white border border-gray-200'
];
$class = $classes[$type] ?? $classes['default'];
$text_class = $type === 'primary' ? 'text-white opacity-90' : 'text-gray-600';
$value_class = $type === 'primary' ? 'text-white' : 'text-gray-900';
?>
<div class="<?php echo esc_attr($class); ?> rounded-lg p-6">
<h3 class="text-sm font-medium <?php echo esc_attr($text_class); ?>"><?php echo esc_html($title); ?></h3>
<div class="text-2xl font-bold <?php echo esc_attr($value_class); ?> mt-2"><?php echo $value; ?></div>
</div>
<?php
}
/**
* Button Component
*/
function sodino_button($text, $url = '#', $type = 'primary', $icon = '') {
$classes = [
'primary' => 'bg-blue-600 text-white hover:bg-blue-700',
'secondary' => 'bg-white text-gray-700 border border-gray-300 hover:bg-gray-50',
'danger' => 'bg-red-600 text-white hover:bg-red-700'
];
$class = $classes[$type] ?? $classes['primary'];
?>
<a href="<?php echo esc_url($url); ?>"
class="inline-flex items-center px-4 py-2 text-sm font-medium rounded-lg <?php echo esc_attr($class); ?> focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
<?php if ($icon): ?>
<svg class="-ml-1 mr-2 h-5 w-5" fill="currentColor" viewBox="0 0 20 20">
<?php echo $icon; ?>
</svg>
<?php endif; ?>
<?php echo esc_html($text); ?>
</a>
<?php
}
/**
* Form Field Component
*/
function sodino_form_field($args) {
$defaults = [
'type' => 'text',
'name' => '',
'label' => '',
'value' => '',
'placeholder' => '',
'required' => false,
'description' => '',
'options' => [],
'class' => ''
];
$args = wp_parse_args($args, $defaults);
extract($args);
$required_attr = $required ? 'required' : '';
$field_id = 'sodino_' . $name;
?>
<div class="<?php echo esc_attr($class); ?>">
<?php if ($label): ?>
<label for="<?php echo esc_attr($field_id); ?>" class="block text-sm font-medium text-gray-700 mb-2">
<?php echo esc_html($label); ?>
<?php if ($required): ?>
<span class="text-red-500">*</span>
<?php endif; ?>
</label>
<?php endif; ?>
<?php if ($type === 'textarea'): ?>
<textarea
id="<?php echo esc_attr($field_id); ?>"
name="<?php echo esc_attr($name); ?>"
rows="4"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
placeholder="<?php echo esc_attr($placeholder); ?>"
<?php echo $required_attr; ?>
><?php echo esc_textarea($value); ?></textarea>
<?php elseif ($type === 'select'): ?>
<select
id="<?php echo esc_attr($field_id); ?>"
name="<?php echo esc_attr($name); ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
<?php echo $required_attr; ?>
>
<?php foreach ($options as $opt_value => $opt_label): ?>
<option value="<?php echo esc_attr($opt_value); ?>" <?php selected($value, $opt_value); ?>>
<?php echo esc_html($opt_label); ?>
</option>
<?php endforeach; ?>
</select>
<?php elseif ($type === 'checkbox'): ?>
<label class="flex items-center">
<input
type="checkbox"
id="<?php echo esc_attr($field_id); ?>"
name="<?php echo esc_attr($name); ?>"
value="1"
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
<?php checked($value, 1); ?>
<?php echo $required_attr; ?>
/>
<span class="mr-2 text-sm text-gray-700"><?php echo esc_html($label); ?></span>
</label>
<?php else: ?>
<input
type="<?php echo esc_attr($type); ?>"
id="<?php echo esc_attr($field_id); ?>"
name="<?php echo esc_attr($name); ?>"
value="<?php echo esc_attr($value); ?>"
placeholder="<?php echo esc_attr($placeholder); ?>"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
<?php echo $required_attr; ?>
/>
<?php endif; ?>
<?php if ($description): ?>
<p class="mt-1 text-sm text-gray-500"><?php echo esc_html($description); ?></p>
<?php endif; ?>
</div>
<?php
}

View File

@@ -0,0 +1,31 @@
<?php
// Prevent direct access
if (!defined('ABSPATH')) {
exit;
}
$current_page = $current_page ?? '';
$menu_items = [
'sodino-dashboard' => __('داشبورد', 'sodino'),
'sodino-rules' => __('قوانین', 'sodino'),
'sodino-add-rule' => __('افزودن قانون', 'sodino'),
'sodino-upsells' => __('آپسل (پیشنهاد فروش)', 'sodino'),
'sodino-add-upsell' => __('افزودن آپسل', 'sodino'),
'sodino-banners' => __('بنرهای هوشمند', 'sodino'),
'sodino-add-banner' => __('افزودن بنر', 'sodino'),
'sodino-settings' => __('تنظیمات', 'sodino'),
];
?>
<aside class="w-64 flex-shrink-0">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<h2 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('منوی سودینو', 'sodino'); ?></h2>
<nav class="space-y-2">
<?php foreach ($menu_items as $page => $label): ?>
<a href="<?php echo admin_url('admin.php?page=' . $page); ?>"
class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === $page ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php echo esc_html($label); ?>
</a>
<?php endforeach; ?>
</nav>
</div>
</aside>

View File

@@ -4,197 +4,185 @@ if (!defined('ABSPATH')) {
exit; exit;
} }
$current_page = sanitize_text_field($_GET['page'] ?? 'sodino-settings'); // Load components
?> require_once SODINO_PLUGIN_DIR . 'admin/components/layout.php';
<div id="sodino-app" class="min-h-screen bg-gray-50" dir="rtl">
<!-- Header --> sodino_admin_layout($current_page ?? 'sodino-settings', function() use ($settings) {
<div class="bg-white border-b border-gray-200"> ?>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> <!-- Page Header -->
<div class="py-6"> <?php sodino_card(
<div class="flex items-center justify-between"> __('تنظیمات سودینو', 'sodino'),
<div> __('تنظیمات عمومی پلاگین را مدیریت کنید.', 'sodino'),
<h1 class="text-3xl font-bold text-gray-900"><?php _e('سودینو', 'sodino'); ?></h1> null,
<p class="mt-1 text-sm text-gray-500"><?php _e('بهینه‌سازی هوشمند فروش', 'sodino'); ?></p> 'mb-8'
); ?>
<!-- Settings Form -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<form method="post" class="space-y-8">
<?php wp_nonce_field('sodino_save_settings', 'sodino_settings_nonce'); ?>
<!-- General Settings -->
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('تنظیمات عمومی', 'sodino'); ?></h3>
<div class="space-y-4">
<?php sodino_form_field([
'type' => 'checkbox',
'name' => 'plugin_enabled',
'label' => __('فعال‌سازی پلاگین', 'sodino'),
'value' => $settings['plugin_enabled'] ?? 1,
'description' => __('پلاگین سودینو را فعال یا غیرفعال کنید.', 'sodino')
]); ?>
<?php sodino_form_field([
'type' => 'checkbox',
'name' => 'pricing_enabled',
'label' => __('فعال‌سازی قیمت‌گذاری پویا', 'sodino'),
'value' => $settings['pricing_enabled'] ?? 1,
'description' => __('قیمت‌گذاری پویا بر اساس قوانین تعریف‌شده.', 'sodino')
]); ?>
<?php sodino_form_field([
'type' => 'checkbox',
'name' => 'upsell_enabled',
'label' => __('فعال‌سازی آپسل', 'sodino'),
'value' => $settings['upsell_enabled'] ?? 1,
'description' => __('نمایش پیشنهادات فروش به مشتریان.', 'sodino')
]); ?>
<?php sodino_form_field([
'type' => 'checkbox',
'name' => 'banner_enabled',
'label' => __('فعال‌سازی بنرهای هوشمند', 'sodino'),
'value' => $settings['banner_enabled'] ?? 1,
'description' => __('نمایش بنرهای هدفمند به کاربران.', 'sodino')
]); ?>
</div>
</div>
<!-- Pricing Strategy -->
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('استراتژی قیمت‌گذاری', 'sodino'); ?></h3>
<div class="space-y-4">
<?php sodino_form_field([
'type' => 'checkbox',
'name' => 'allow_multiple_rules',
'label' => __('اجازه اعمال چند قانون همزمان', 'sodino'),
'value' => $settings['allow_multiple_rules'] ?? 0,
'description' => __('اگر فعال باشد، چند قانون می‌تواند روی یک محصول اعمال شود.', 'sodino')
]); ?>
<?php sodino_form_field([
'type' => 'select',
'name' => 'strategy',
'label' => __('استراتژی انتخاب قانون', 'sodino'),
'value' => $settings['strategy'] ?? 'priority',
'options' => [
'priority' => __('بر اساس اولویت', 'sodino'),
'highest_discount' => __('بیشترین تخفیف', 'sodino'),
'first_valid' => __('اولین قانون معتبر', 'sodino')
],
'description' => __('نحوه انتخاب قانون زمانی که چند قانون معتبر وجود دارد.', 'sodino')
]); ?>
<?php sodino_form_field([
'type' => 'number',
'name' => 'max_discount_percent',
'label' => __('حداکثر درصد تخفیف', 'sodino'),
'value' => $settings['max_discount_percent'] ?? 100,
'placeholder' => '100',
'description' => __('حداکثر درصد تخفیفی که می‌تواند اعمال شود (0-100).', 'sodino')
]); ?>
<?php sodino_form_field([
'type' => 'number',
'name' => 'min_product_price',
'label' => __('حداقل قیمت محصول', 'sodino'),
'value' => $settings['min_product_price'] ?? 0,
'placeholder' => '0',
'description' => __('حداقل قیمتی که یک محصول می‌تواند داشته باشد.', 'sodino')
]); ?>
<?php sodino_form_field([
'type' => 'checkbox',
'name' => 'cart_pricing_enabled',
'label' => __('قیمت‌گذاری در سبد خرید', 'sodino'),
'value' => $settings['cart_pricing_enabled'] ?? 1,
'description' => __('اعمال قیمت‌گذاری پویا در صفحه سبد خرید.', 'sodino')
]); ?>
</div>
</div>
<!-- Performance Settings -->
<div class="border-b border-gray-200 pb-6">
<h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('تنظیمات عملکرد', 'sodino'); ?></h3>
<div class="space-y-4">
<?php sodino_form_field([
'type' => 'checkbox',
'name' => 'cache_enabled',
'label' => __('فعال‌سازی کش', 'sodino'),
'value' => $settings['cache_enabled'] ?? 1,
'description' => __('استفاده از کش برای بهبود عملکرد.', 'sodino')
]); ?>
<?php sodino_form_field([
'type' => 'number',
'name' => 'cache_duration',
'label' => __('مدت زمان کش (ثانیه)', 'sodino'),
'value' => $settings['cache_duration'] ?? 3600,
'placeholder' => '3600',
'description' => __('مدت زمان نگهداری داده‌ها در کش (پیش‌فرض: 3600 ثانیه = 1 ساعت).', 'sodino')
]); ?>
<div class="flex items-center gap-4">
<?php sodino_button(
__('پاک کردن کش', 'sodino'),
wp_nonce_url(admin_url('admin.php?page=sodino-settings&action=clear_cache'), 'clear_cache'),
'secondary'
); ?>
<span class="text-sm text-gray-500"><?php _e('تمام کش‌های سودینو را پاک می‌کند.', 'sodino'); ?></span>
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <!-- Advanced Settings -->
<div class="flex gap-8"> <div>
<!-- Sidebar --> <h3 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('تنظیمات پیشرفته', 'sodino'); ?></h3>
<aside class="w-64 flex-shrink-0"> <div class="space-y-4">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6"> <?php sodino_form_field([
<h2 class="text-lg font-semibold text-gray-900 mb-4"><?php _e('منوی سودینو', 'sodino'); ?></h2> 'type' => 'checkbox',
<nav class="space-y-2"> 'name' => 'ab_testing_enabled',
<a href="<?php echo admin_url('admin.php?page=sodino-dashboard'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-dashboard' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> 'label' => __('فعال‌سازی A/B Testing', 'sodino'),
<?php _e('داشبورد', 'sodino'); ?> 'value' => $settings['ab_testing_enabled'] ?? 0,
</a> 'description' => __('تست A/B برای قوانین قیمت‌گذاری (قابلیت آزمایشی).', 'sodino')
<a href="<?php echo admin_url('admin.php?page=sodino-rules'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-rules' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> ]); ?>
<?php _e('قوانین', 'sodino'); ?>
</a> <?php sodino_form_field([
<a href="<?php echo admin_url('admin.php?page=sodino-add-rule'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-rule' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> 'type' => 'checkbox',
<?php _e('افزودن قانون', 'sodino'); ?> 'name' => 'scheduled_campaigns_enabled',
</a> 'label' => __('فعال‌سازی کمپین‌های زمان‌بندی شده', 'sodino'),
<a href="<?php echo admin_url('admin.php?page=sodino-upsells'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-upsells' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> 'value' => $settings['scheduled_campaigns_enabled'] ?? 1,
<?php _e('آپسل (پیشنهاد فروش)', 'sodino'); ?> 'description' => __('اجرای خودکار قوانین بر اساس تاریخ شروع و پایان.', 'sodino')
</a> ]); ?>
<a href="<?php echo admin_url('admin.php?page=sodino-add-upsell'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-add-upsell' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>">
<?php _e('افزودن آپسل', 'sodino'); ?> <?php sodino_form_field([
</a> 'type' => 'checkbox',
<a href="<?php echo admin_url('admin.php?page=sodino-competitor-price'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-competitor-price' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> 'name' => 'debug_mode',
<?php _e('قیمت رقبا (به‌زودی)', 'sodino'); ?> 'label' => __('حالت دیباگ', 'sodino'),
</a> 'value' => $settings['debug_mode'] ?? 0,
<a href="<?php echo admin_url('admin.php?page=sodino-settings'); ?>" class="block px-3 py-2 rounded-md text-sm font-medium <?php echo $current_page === 'sodino-settings' ? 'bg-blue-50 text-blue-700 border-r-2 border-blue-700' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'; ?>"> 'description' => __('فعال‌سازی لاگ‌های دیباگ (فقط برای توسعه‌دهندگان).', 'sodino')
<?php _e('تنظیمات', 'sodino'); ?> ]); ?>
</a>
</nav>
</div> </div>
</aside> </div>
<!-- Main Content --> <!-- Submit Button -->
<main class="flex-1 min-w-0"> <div class="flex items-center justify-end gap-4 pt-6 border-t border-gray-200">
<!-- Page Header --> <button type="submit" class="inline-flex items-center px-6 py-3 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6 mb-8"> <?php _e('ذخیره تنظیمات', 'sodino'); ?>
<h2 class="text-2xl font-semibold text-gray-900"><?php _e('تنظیمات سودینو', 'sodino'); ?></h2> </button>
<p class="mt-2 text-gray-600"><?php _e('کنترل کامل تجربه قیمت‌گذاری و بهینه‌سازی درآمد را از اینجا انجام دهید.', 'sodino'); ?></p> </div>
</div> </form>
<?php if (isset($_GET['updated']) && $_GET['updated'] === 'true') : ?>
<div class="mb-6 bg-green-50 border border-green-200 rounded-lg p-4">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
</div>
<div class="mr-3">
<p class="text-sm font-medium text-green-800"><?php _e('تنظیمات با موفقیت ذخیره شد.', 'sodino'); ?></p>
</div>
</div>
</div>
<?php endif; ?>
<form method="post" class="space-y-6">
<?php wp_nonce_field('sodino_save_settings', 'sodino_settings_nonce'); ?>
<!-- General Section -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="mb-6">
<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-6 md:grid-cols-2">
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
<label class="flex items-center gap-3 text-gray-700">
<input type="checkbox" name="plugin_enabled" value="1" <?php checked($settings['plugin_enabled'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span><?php _e('فعال‌سازی کل پلاگین', 'sodino'); ?></span>
</label>
<p class="mt-3 text-sm text-gray-500"><?php _e('اگر غیرفعال باشد، هیچ قاعده‌ای اعمال نخواهد شد.', 'sodino'); ?></p>
</div>
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
<label class="flex items-center gap-3 text-gray-700">
<input type="checkbox" name="pricing_enabled" value="1" <?php checked($settings['pricing_enabled'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span><?php _e('فعال‌سازی قیمت‌گذاری پویا', 'sodino'); ?></span>
</label>
<p class="mt-3 text-sm text-gray-500"><?php _e('این گزینه، اعمال قوانین قیمت‌گذاری را کنترل می‌کند.', 'sodino'); ?></p>
</div>
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
<label class="flex items-center gap-3 text-gray-700">
<input type="checkbox" name="upsell_enabled" value="1" <?php checked($settings['upsell_enabled'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span><?php _e('فعال‌سازی سیستم آپسل', 'sodino'); ?></span>
</label>
<p class="mt-3 text-sm text-gray-500"><?php _e('پیشنهادهای درآمدی اضافه را نمایش می‌دهد.', 'sodino'); ?></p>
</div>
</div>
</div>
<!-- Pricing Behavior Section -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="mb-6">
<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-6 md:grid-cols-2">
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
<label class="flex items-center gap-3 text-gray-700">
<input type="checkbox" name="allow_multiple_rules" value="1" <?php checked($settings['allow_multiple_rules'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span><?php _e('اجازه اعمال چند قانون همزمان', 'sodino'); ?></span>
</label>
<p class="mt-3 text-sm text-gray-500"><?php _e('قوانین معتبر به صورت متوالی اعمال می‌شوند.', 'sodino'); ?></p>
</div>
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
<label class="block text-sm font-medium text-gray-700 mb-3"><?php _e('استراتژی اعمال', 'sodino'); ?></label>
<select name="strategy" class="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-100">
<option value="highest_discount" <?php selected($settings['strategy'], 'highest_discount'); ?>><?php _e('بالاترین تخفیف', 'sodino'); ?></option>
<option value="first_valid" <?php selected($settings['strategy'], 'first_valid'); ?>><?php _e('اولین قانون معتبر', 'sodino'); ?></option>
<option value="priority" <?php selected($settings['strategy'], 'priority'); ?>><?php _e('بر اساس اولویت', 'sodino'); ?></option>
</select>
<p class="mt-3 text-sm text-gray-500"><?php _e('استراتژی انتخاب قانون زمانی که بیش از یک قانون معتبر وجود داشته باشد.', 'sodino'); ?></p>
</div>
</div>
</div>
<!-- Limits Section -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="mb-6">
<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-6 md:grid-cols-2">
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
<label class="block text-sm font-medium text-gray-700 mb-3"><?php _e('حداکثر درصد تخفیف', 'sodino'); ?></label>
<input type="number" name="max_discount_percent" value="<?php echo esc_attr($settings['max_discount_percent']); ?>" min="0" max="100" class="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-100">
<p class="mt-3 text-sm text-gray-500"><?php _e('حداکثر تخفیف مجاز برای هر محصول را تعیین می‌کند.', 'sodino'); ?></p>
</div>
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
<label class="block text-sm font-medium text-gray-700 mb-3"><?php _e('حداقل قیمت محصول', 'sodino'); ?></label>
<input type="number" name="min_product_price" value="<?php echo esc_attr($settings['min_product_price']); ?>" min="0" step="0.01" class="w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-100">
<p class="mt-3 text-sm text-gray-500"><?php _e('از کاهش قیمت زیر این مقدار جلوگیری می‌کند.', 'sodino'); ?></p>
</div>
</div>
</div>
<!-- Features Section -->
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
<div class="mb-6">
<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-6 md:grid-cols-2">
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
<label class="flex items-center gap-3 text-gray-700">
<input type="checkbox" name="ab_testing_enabled" value="1" <?php checked($settings['ab_testing_enabled'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span><?php _e('فعال‌سازی تست A/B', 'sodino'); ?></span>
</label>
<p class="mt-3 text-sm text-gray-500"><?php _e('امکان فعال‌سازی سناریوهای آزمایشی را اضافه می‌کند.', 'sodino'); ?></p>
</div>
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
<label class="flex items-center gap-3 text-gray-700">
<input type="checkbox" name="cart_pricing_enabled" value="1" <?php checked($settings['cart_pricing_enabled'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span><?php _e('اعمال قیمت‌گذاری در سبد خرید', 'sodino'); ?></span>
</label>
<p class="mt-3 text-sm text-gray-500"><?php _e('در صورت خاموش بودن، قیمت‌گذاری پویا فقط در نمایش محصول انجام می‌شود.', 'sodino'); ?></p>
</div>
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200">
<label class="flex items-center gap-3 text-gray-700">
<input type="checkbox" name="scheduled_campaigns_enabled" value="1" <?php checked($settings['scheduled_campaigns_enabled'], 1); ?> class="h-5 w-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500">
<span><?php _e('فعال‌سازی کمپین‌های زمان‌بندی شده', 'sodino'); ?></span>
</label>
<p class="mt-3 text-sm text-gray-500"><?php _e('با فعال کردن، می‌توانید قوانین را به صورت زمان‌بندی‌شده اجرا کنید.', 'sodino'); ?></p>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="flex justify-end">
<button type="submit" class="inline-flex items-center px-6 py-3 border border-transparent text-sm font-medium rounded-lg text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors duration-200">
<?php _e('ذخیره تنظیمات', 'sodino'); ?>
</button>
</div>
</form>
</main>
</div>
</div> </div>
</div> <?php
});
?>

View File

@@ -0,0 +1,94 @@
<?php
namespace Sodino\Controllers;
use Sodino\Core\Validator;
/**
* Base Controller
*/
abstract class BaseController {
/**
* Verify nonce
*/
protected function verifyNonce($nonce_field, $nonce_action) {
if (!isset($_POST[$nonce_field]) || !wp_verify_nonce($_POST[$nonce_field], $nonce_action)) {
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
}
}
/**
* Redirect with message
*/
protected function redirect($url, $message = '', $type = 'success') {
if ($message) {
set_transient('sodino_admin_notice', [
'message' => $message,
'type' => $type
], 30);
}
wp_safe_redirect($url);
exit;
}
/**
* Get sanitized POST data
*/
protected function getPostData($key, $default = '') {
return isset($_POST[$key]) ? sanitize_text_field($_POST[$key]) : $default;
}
/**
* Get sanitized GET data
*/
protected function getQueryData($key, $default = '') {
return isset($_GET[$key]) ? sanitize_text_field($_GET[$key]) : $default;
}
/**
* Validate data
*/
protected function validate(array $data) {
return Validator::make($data);
}
/**
* Render view
*/
protected function render($view, $data = []) {
extract($data);
$view_file = SODINO_PLUGIN_DIR . 'admin/views/' . $view . '.php';
if (file_exists($view_file)) {
include $view_file;
} else {
wp_die(sprintf(__('View file not found: %s', 'sodino'), $view));
}
}
/**
* Check user capability
*/
protected function checkCapability($capability = 'manage_options') {
if (!current_user_can($capability)) {
wp_die(__('شما دسترسی لازم برای این عملیات را ندارید.', 'sodino'));
}
}
/**
* Show admin notice
*/
public function showAdminNotice() {
$notice = get_transient('sodino_admin_notice');
if ($notice) {
$class = $notice['type'] === 'error' ? 'notice-error' : 'notice-success';
printf(
'<div class="notice %s is-dismissible"><p>%s</p></div>',
esc_attr($class),
esc_html($notice['message'])
);
delete_transient('sodino_admin_notice');
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Sodino\Controllers;
use Sodino\Repositories\EventRepository;
use Sodino\Repositories\RuleRepository;
use Sodino\Services\AnalyticsService;
/**
* Dashboard Controller
*/
class DashboardController extends BaseController {
private $analyticsService;
public function __construct(EventRepository $eventRepository, RuleRepository $ruleRepository) {
$this->analyticsService = new AnalyticsService($eventRepository, $ruleRepository);
}
/**
* Dashboard page
*/
public function index() {
$this->checkCapability();
$filters = [
'range' => $this->getQueryData('range', '7d'),
'start_date' => $this->getQueryData('start_date', ''),
'end_date' => $this->getQueryData('end_date', ''),
'product_id' => intval($this->getQueryData('product_id', 0)),
'category_id' => intval($this->getQueryData('category_id', 0)),
];
if (!empty($filters['product_id'])) {
$filters['product_ids'] = [$filters['product_id']];
}
$dashboardData = $this->analyticsService->getDashboardData($filters);
$productOptions = $this->analyticsService->getProductOptions();
$categoryOptions = $this->analyticsService->getCategoryOptions();
$this->render('dashboard', [
'dashboardData' => $dashboardData,
'productOptions' => $productOptions,
'categoryOptions' => $categoryOptions,
'filters' => $filters,
'current_page' => 'sodino-dashboard'
]);
}
}

View File

@@ -0,0 +1,189 @@
<?php
namespace Sodino\Controllers;
use Sodino\Repositories\RuleRepository;
use Sodino\Models\Rule;
/**
* Rule Controller
*/
class RuleController extends BaseController {
private $ruleRepository;
public function __construct(RuleRepository $ruleRepository) {
$this->ruleRepository = $ruleRepository;
}
/**
* List rules page
*/
public function index() {
$this->checkCapability();
require_once SODINO_PLUGIN_DIR . 'admin/class-rules-list-table.php';
$rulesTable = new \Sodino_Rules_List_Table($this->ruleRepository);
$rulesTable->prepare_items();
$this->render('rules-list', [
'rulesTable' => $rulesTable,
'current_page' => 'sodino-rules'
]);
}
/**
* Create rule page
*/
public function create() {
$this->checkCapability();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
return $this->store();
}
$rule = new Rule();
$this->render('rule-form', [
'rule' => $rule,
'current_page' => 'sodino-add-rule'
]);
}
/**
* Edit rule page
*/
public function edit() {
$this->checkCapability();
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$rule = $this->ruleRepository->getById($id);
if (!$rule) {
$this->redirect(
admin_url('admin.php?page=sodino-rules'),
__('قانون یافت نشد.', 'sodino'),
'error'
);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
return $this->update($rule);
}
$this->render('rule-form', [
'rule' => $rule,
'current_page' => 'sodino-add-rule'
]);
}
/**
* Store new rule
*/
private function store() {
$this->verifyNonce('sodino_rule_nonce', 'sodino_save_rule');
$validator = $this->validate($_POST);
$validator->required('name', __('نام قانون الزامی است.', 'sodino'))
->numeric('priority')
->min('priority', 1)
->numeric('usage_limit')
->min('usage_limit', 0);
if ($validator->fails()) {
$this->redirect(
admin_url('admin.php?page=sodino-add-rule'),
$validator->firstError(),
'error'
);
}
$rule = new Rule();
$this->fillRuleFromPost($rule);
$this->ruleRepository->save($rule);
$this->redirect(
admin_url('admin.php?page=sodino-rules'),
__('قانون با موفقیت ایجاد شد.', 'sodino')
);
}
/**
* Update existing rule
*/
private function update($rule) {
$this->verifyNonce('sodino_rule_nonce', 'sodino_save_rule');
$validator = $this->validate($_POST);
$validator->required('name', __('نام قانون الزامی است.', 'sodino'))
->numeric('priority')
->min('priority', 1)
->numeric('usage_limit')
->min('usage_limit', 0);
if ($validator->fails()) {
$this->redirect(
admin_url('admin.php?page=sodino-add-rule&action=edit&id=' . $rule->id),
$validator->firstError(),
'error'
);
}
$this->fillRuleFromPost($rule);
$this->ruleRepository->save($rule);
$this->redirect(
admin_url('admin.php?page=sodino-rules'),
__('قانون با موفقیت به‌روزرسانی شد.', 'sodino')
);
}
/**
* Delete rule
*/
public function delete() {
$this->checkCapability();
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'delete_rule')) {
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
}
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
$this->ruleRepository->delete($id);
$this->redirect(
admin_url('admin.php?page=sodino-rules'),
__('قانون با موفقیت حذف شد.', 'sodino')
);
}
/**
* Fill rule from POST data
*/
private function fillRuleFromPost($rule) {
$rule->name = sanitize_text_field($_POST['name'] ?? '');
$rule->priority = max(1, intval($_POST['priority'] ?? 10));
$rule->usage_limit = max(0, intval($_POST['usage_limit'] ?? 0));
$rule->user_roles = array_map('sanitize_text_field', (array) ($_POST['user_roles'] ?? []));
$rule->start_date = !empty($_POST['start_date']) ? sanitize_text_field($_POST['start_date']) : null;
$rule->end_date = !empty($_POST['end_date']) ? sanitize_text_field($_POST['end_date']) : null;
$rule->enabled = isset($_POST['enabled']) ? 1 : 0;
// Parse conditions
if (isset($_POST['conditions']) && is_array($_POST['conditions'])) {
$rule->conditions = array_map(function($condition) {
return [
'type' => sanitize_text_field($condition['type'] ?? ''),
'value' => sanitize_text_field($condition['value'] ?? '')
];
}, $_POST['conditions']);
}
// Parse actions
if (isset($_POST['actions']) && is_array($_POST['actions'])) {
$rule->actions = array_map(function($action) {
return [
'type' => sanitize_text_field($action['type'] ?? ''),
'value' => sanitize_text_field($action['value'] ?? '')
];
}, $_POST['actions']);
}
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Sodino\Controllers;
use Sodino\Core\Settings;
use Sodino\Core\Cache;
/**
* Settings Controller
*/
class SettingsController extends BaseController {
private $settings;
private $cache;
public function __construct() {
$this->settings = Settings::getInstance();
$this->cache = Cache::getInstance();
}
/**
* Settings page
*/
public function index() {
$this->checkCapability();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
return $this->save();
}
$settings = $this->settings->all();
$this->render('settings', [
'settings' => $settings,
'current_page' => 'sodino-settings'
]);
}
/**
* Save settings
*/
private function save() {
$this->verifyNonce('sodino_settings_nonce', 'sodino_save_settings');
$settings = [
'plugin_enabled' => isset($_POST['plugin_enabled']) ? 1 : 0,
'pricing_enabled' => isset($_POST['pricing_enabled']) ? 1 : 0,
'upsell_enabled' => isset($_POST['upsell_enabled']) ? 1 : 0,
'banner_enabled' => isset($_POST['banner_enabled']) ? 1 : 0,
'allow_multiple_rules' => isset($_POST['allow_multiple_rules']) ? 1 : 0,
'strategy' => sanitize_text_field($_POST['strategy'] ?? 'priority'),
'max_discount_percent' => max(0, min(100, floatval($_POST['max_discount_percent'] ?? 100))),
'min_product_price' => max(0, floatval($_POST['min_product_price'] ?? 0)),
'ab_testing_enabled' => isset($_POST['ab_testing_enabled']) ? 1 : 0,
'cart_pricing_enabled' => isset($_POST['cart_pricing_enabled']) ? 1 : 0,
'scheduled_campaigns_enabled' => isset($_POST['scheduled_campaigns_enabled']) ? 1 : 0,
'cache_enabled' => isset($_POST['cache_enabled']) ? 1 : 0,
'cache_duration' => max(60, intval($_POST['cache_duration'] ?? 3600)),
'debug_mode' => isset($_POST['debug_mode']) ? 1 : 0,
];
$this->settings->update($settings);
// Clear cache when settings change
$this->cache->clearAll();
$this->redirect(
admin_url('admin.php?page=sodino-settings'),
__('تنظیمات با موفقیت ذخیره شد.', 'sodino')
);
}
/**
* Clear cache action
*/
public function clearCache() {
$this->checkCapability();
if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'clear_cache')) {
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
}
$this->cache->clearAll();
$this->redirect(
admin_url('admin.php?page=sodino-settings'),
__('کش با موفقیت پاک شد.', 'sodino')
);
}
}

137
app/Core/Cache.php Normal file
View File

@@ -0,0 +1,137 @@
<?php
namespace Sodino\Core;
/**
* Cache Manager
* Handles caching with WordPress transients and custom database cache
*/
class Cache {
private static $instance = null;
private $memory_cache = [];
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get cached value
*/
public function get($key, $group = 'sodino') {
$full_key = $this->buildKey($key, $group);
// Check memory cache first
if (isset($this->memory_cache[$full_key])) {
return $this->memory_cache[$full_key];
}
// Check WordPress transient
$value = get_transient($full_key);
if ($value !== false) {
$this->memory_cache[$full_key] = $value;
return $value;
}
return false;
}
/**
* Set cached value
*/
public function set($key, $value, $expiration = 3600, $group = 'sodino') {
$full_key = $this->buildKey($key, $group);
// Set in memory cache
$this->memory_cache[$full_key] = $value;
// Set in WordPress transient
return set_transient($full_key, $value, $expiration);
}
/**
* Delete cached value
*/
public function delete($key, $group = 'sodino') {
$full_key = $this->buildKey($key, $group);
// Remove from memory cache
unset($this->memory_cache[$full_key]);
// Remove from WordPress transient
return delete_transient($full_key);
}
/**
* Clear all cache for a group
*/
public function clearGroup($group = 'sodino') {
global $wpdb;
// Clear memory cache for group
foreach ($this->memory_cache as $key => $value) {
if (strpos($key, "sodino_{$group}_") === 0) {
unset($this->memory_cache[$key]);
}
}
// Clear transients for group
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like('_transient_sodino_' . $group . '_') . '%'
)
);
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
$wpdb->esc_like('_transient_timeout_sodino_' . $group . '_') . '%'
)
);
return true;
}
/**
* Clear all Sodino cache
*/
public function clearAll() {
global $wpdb;
// Clear memory cache
$this->memory_cache = [];
// Clear all Sodino transients
$wpdb->query(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_sodino_%' OR option_name LIKE '_transient_timeout_sodino_%'"
);
return true;
}
/**
* Remember pattern - get from cache or execute callback
*/
public function remember($key, $callback, $expiration = 3600, $group = 'sodino') {
$value = $this->get($key, $group);
if ($value !== false) {
return $value;
}
$value = call_user_func($callback);
$this->set($key, $value, $expiration, $group);
return $value;
}
/**
* Build cache key
*/
private function buildKey($key, $group) {
return "sodino_{$group}_{$key}";
}
}

125
app/Core/Settings.php Normal file
View File

@@ -0,0 +1,125 @@
<?php
namespace Sodino\Core;
/**
* Settings Manager
*/
class Settings {
private static $instance = null;
private $settings = null;
private $option_name = 'sodino_settings';
private $defaults = [
'plugin_enabled' => 1,
'pricing_enabled' => 1,
'upsell_enabled' => 1,
'banner_enabled' => 1,
'allow_multiple_rules' => 0,
'strategy' => 'priority',
'max_discount_percent' => 100,
'min_product_price' => 0,
'ab_testing_enabled' => 0,
'cart_pricing_enabled' => 1,
'scheduled_campaigns_enabled' => 1,
'cache_enabled' => 1,
'cache_duration' => 3600,
'debug_mode' => 0,
];
public static function getInstance() {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Get all settings
*/
public function all() {
if ($this->settings === null) {
$this->settings = wp_parse_args(
get_option($this->option_name, []),
$this->defaults
);
}
return $this->settings;
}
/**
* Get single setting
*/
public function get($key, $default = null) {
$settings = $this->all();
return $settings[$key] ?? $default ?? $this->defaults[$key] ?? null;
}
/**
* Set single setting
*/
public function set($key, $value) {
$settings = $this->all();
$settings[$key] = $value;
$this->settings = $settings;
return update_option($this->option_name, $settings);
}
/**
* Update multiple settings
*/
public function update(array $settings) {
$current = $this->all();
$this->settings = array_merge($current, $settings);
return update_option($this->option_name, $this->settings);
}
/**
* Reset to defaults
*/
public function reset() {
$this->settings = $this->defaults;
return update_option($this->option_name, $this->defaults);
}
/**
* Check if plugin is enabled
*/
public function isEnabled() {
return (bool) $this->get('plugin_enabled');
}
/**
* Check if pricing is enabled
*/
public function isPricingEnabled() {
return $this->isEnabled() && (bool) $this->get('pricing_enabled');
}
/**
* Check if upsell is enabled
*/
public function isUpsellEnabled() {
return $this->isEnabled() && (bool) $this->get('upsell_enabled');
}
/**
* Check if banner is enabled
*/
public function isBannerEnabled() {
return $this->isEnabled() && (bool) $this->get('banner_enabled');
}
/**
* Check if cache is enabled
*/
public function isCacheEnabled() {
return (bool) $this->get('cache_enabled');
}
/**
* Check if debug mode is enabled
*/
public function isDebugMode() {
return (bool) $this->get('debug_mode');
}
}

132
app/Core/Validator.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
namespace Sodino\Core;
/**
* Validation Helper
*/
class Validator {
private $errors = [];
private $data = [];
public function __construct(array $data) {
$this->data = $data;
}
/**
* Validate required field
*/
public function required($field, $message = null) {
if (!isset($this->data[$field]) || empty($this->data[$field])) {
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s الزامی است.', 'sodino'), $field);
}
return $this;
}
/**
* Validate numeric field
*/
public function numeric($field, $message = null) {
if (isset($this->data[$field]) && !is_numeric($this->data[$field])) {
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s باید عدد باشد.', 'sodino'), $field);
}
return $this;
}
/**
* Validate min value
*/
public function min($field, $min, $message = null) {
if (isset($this->data[$field]) && $this->data[$field] < $min) {
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s باید حداقل %s باشد.', 'sodino'), $field, $min);
}
return $this;
}
/**
* Validate max value
*/
public function max($field, $max, $message = null) {
if (isset($this->data[$field]) && $this->data[$field] > $max) {
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s باید حداکثر %s باشد.', 'sodino'), $field, $max);
}
return $this;
}
/**
* Validate email
*/
public function email($field, $message = null) {
if (isset($this->data[$field]) && !filter_var($this->data[$field], FILTER_VALIDATE_EMAIL)) {
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s باید یک ایمیل معتبر باشد.', 'sodino'), $field);
}
return $this;
}
/**
* Validate URL
*/
public function url($field, $message = null) {
if (isset($this->data[$field]) && !filter_var($this->data[$field], FILTER_VALIDATE_URL)) {
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s باید یک URL معتبر باشد.', 'sodino'), $field);
}
return $this;
}
/**
* Validate in array
*/
public function in($field, array $values, $message = null) {
if (isset($this->data[$field]) && !in_array($this->data[$field], $values, true)) {
$this->errors[$field][] = $message ?? sprintf(__('مقدار فیلد %s نامعتبر است.', 'sodino'), $field);
}
return $this;
}
/**
* Custom validation
*/
public function custom($field, callable $callback, $message = null) {
if (isset($this->data[$field]) && !call_user_func($callback, $this->data[$field])) {
$this->errors[$field][] = $message ?? sprintf(__('فیلد %s نامعتبر است.', 'sodino'), $field);
}
return $this;
}
/**
* Check if validation passed
*/
public function passes() {
return empty($this->errors);
}
/**
* Check if validation failed
*/
public function fails() {
return !$this->passes();
}
/**
* Get all errors
*/
public function errors() {
return $this->errors;
}
/**
* Get first error
*/
public function firstError() {
foreach ($this->errors as $field => $messages) {
return $messages[0] ?? '';
}
return '';
}
/**
* Static factory method
*/
public static function make(array $data) {
return new self($data);
}
}

View File

@@ -11,14 +11,11 @@ class Rule {
public $actions; public $actions;
public $priority; public $priority;
public $usage_limit; public $usage_limit;
public $usage_count;
public $user_roles; public $user_roles;
public $start_date; public $start_date;
public $end_date; public $end_date;
public $enabled; public $enabled;
public $condition_type;
public $condition_value;
public $action_type;
public $action_value;
public $created_at; public $created_at;
public $updated_at; public $updated_at;
@@ -32,28 +29,13 @@ class Rule {
$this->actions = $this->parseJsonField($data['actions'] ?? '[]'); $this->actions = $this->parseJsonField($data['actions'] ?? '[]');
$this->priority = isset($data['priority']) ? (int) $data['priority'] : 10; $this->priority = isset($data['priority']) ? (int) $data['priority'] : 10;
$this->usage_limit = isset($data['usage_limit']) ? (int) $data['usage_limit'] : 0; $this->usage_limit = isset($data['usage_limit']) ? (int) $data['usage_limit'] : 0;
$this->usage_count = isset($data['usage_count']) ? (int) $data['usage_count'] : 0;
$this->user_roles = $this->parseRolesField($data['user_roles'] ?? ''); $this->user_roles = $this->parseRolesField($data['user_roles'] ?? '');
$this->start_date = $data['start_date'] ?? null; $this->start_date = $data['start_date'] ?? null;
$this->end_date = $data['end_date'] ?? null; $this->end_date = $data['end_date'] ?? null;
$this->enabled = isset($data['enabled']) ? (int) $data['enabled'] : 1; $this->enabled = isset($data['enabled']) ? (int) $data['enabled'] : 1;
$this->condition_type = $data['condition_type'] ?? '';
$this->condition_value = $data['condition_value'] ?? '';
$this->action_type = $data['action_type'] ?? '';
$this->action_value = $data['action_value'] ?? '';
$this->created_at = $data['created_at'] ?? null; $this->created_at = $data['created_at'] ?? null;
$this->updated_at = $data['updated_at'] ?? null; $this->updated_at = $data['updated_at'] ?? null;
if (empty($this->conditions) && !empty($this->condition_type)) {
$this->conditions = [
['type' => $this->condition_type, 'value' => $this->condition_value],
];
}
if (empty($this->actions) && !empty($this->action_type)) {
$this->actions = [
['type' => $this->action_type, 'value' => $this->action_value],
];
}
} }
private function parseJsonField($value) { private function parseJsonField($value) {
@@ -88,16 +70,41 @@ class Rule {
'actions' => wp_json_encode($this->actions), 'actions' => wp_json_encode($this->actions),
'priority' => $this->priority, 'priority' => $this->priority,
'usage_limit' => $this->usage_limit, 'usage_limit' => $this->usage_limit,
'usage_count' => $this->usage_count,
'user_roles' => is_array($this->user_roles) ? implode(',', $this->user_roles) : $this->user_roles, 'user_roles' => is_array($this->user_roles) ? implode(',', $this->user_roles) : $this->user_roles,
'start_date' => $this->start_date, 'start_date' => $this->start_date,
'end_date' => $this->end_date, 'end_date' => $this->end_date,
'enabled' => $this->enabled, 'enabled' => $this->enabled,
'condition_type' => $this->condition_type,
'condition_value' => $this->condition_value,
'action_type' => $this->action_type,
'action_value' => $this->action_value,
'created_at' => $this->created_at, 'created_at' => $this->created_at,
'updated_at' => $this->updated_at, 'updated_at' => $this->updated_at,
]; ];
} }
/**
* Check if rule is active
*/
public function isActive() {
if (!$this->enabled) {
return false;
}
$now = current_time('mysql');
if (!empty($this->start_date) && $now < $this->start_date) {
return false;
}
if (!empty($this->end_date) && $now > $this->end_date) {
return false;
}
return true;
}
/**
* Check if usage limit reached
*/
public function hasReachedLimit() {
return $this->usage_limit > 0 && $this->usage_count >= $this->usage_limit;
}
} }

View File

@@ -2,51 +2,80 @@
namespace Sodino\Repositories; namespace Sodino\Repositories;
use Sodino\Models\Rule; use Sodino\Models\Rule;
use Sodino\Core\Cache;
/** /**
* Rule Repository * Rule Repository
*/ */
class RuleRepository { class RuleRepository {
private $table_name; private $table_name;
private $cache;
private $cache_group = 'rules';
private $cache_duration = 3600;
public function __construct() { public function __construct() {
global $wpdb; global $wpdb;
$this->table_name = $wpdb->prefix . 'sodino_rules'; $this->table_name = $wpdb->prefix . 'sodino_rules';
$this->cache = Cache::getInstance();
} }
/** /**
* Get all rules * Get all rules
*/ */
public function getAll() { public function getAll() {
global $wpdb; return $this->cache->remember('all_rules', function() {
$results = $wpdb->get_results("SELECT * FROM {$this->table_name} ORDER BY priority DESC, id ASC", ARRAY_A); global $wpdb;
$rules = []; $results = $wpdb->get_results(
foreach ($results as $result) { "SELECT * FROM {$this->table_name} ORDER BY priority DESC, id ASC",
$rules[] = new Rule($result); ARRAY_A
} );
return $rules; $rules = [];
foreach ($results as $result) {
$rules[] = new Rule($result);
}
return $rules;
}, $this->cache_duration, $this->cache_group);
} }
/** /**
* Get rule by ID * Get rule by ID
*/ */
public function getById($id) { public function getById($id) {
global $wpdb; return $this->cache->remember("rule_{$id}", function() use ($id) {
$result = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id), ARRAY_A); global $wpdb;
return $result ? new Rule($result) : null; $result = $wpdb->get_row(
$wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id),
ARRAY_A
);
return $result ? new Rule($result) : null;
}, $this->cache_duration, $this->cache_group);
} }
/** /**
* Get enabled rules * Get enabled rules
*/ */
public function getEnabled() { public function getEnabled() {
global $wpdb; return $this->cache->remember('enabled_rules', function() {
$results = $wpdb->get_results("SELECT * FROM {$this->table_name} WHERE enabled = 1 ORDER BY priority DESC, id ASC", ARRAY_A); global $wpdb;
$rules = []; $now = current_time('mysql');
foreach ($results as $result) { $results = $wpdb->get_results(
$rules[] = new Rule($result); $wpdb->prepare(
} "SELECT * FROM {$this->table_name}
return $rules; WHERE enabled = 1
AND (start_date IS NULL OR start_date <= %s)
AND (end_date IS NULL OR end_date >= %s)
ORDER BY priority DESC, id ASC",
$now,
$now
),
ARRAY_A
);
$rules = [];
foreach ($results as $result) {
$rules[] = new Rule($result);
}
return $rules;
}, $this->cache_duration, $this->cache_group);
} }
/** /**
@@ -59,11 +88,16 @@ class RuleRepository {
if ($rule->id) { if ($rule->id) {
$wpdb->update($this->table_name, $data, ['id' => $rule->id]); $wpdb->update($this->table_name, $data, ['id' => $rule->id]);
return $rule->id; $id = $rule->id;
} else { } else {
$wpdb->insert($this->table_name, $data); $wpdb->insert($this->table_name, $data);
return $wpdb->insert_id; $id = $wpdb->insert_id;
} }
// Clear cache
$this->clearCache();
return $id;
} }
/** /**
@@ -71,6 +105,31 @@ class RuleRepository {
*/ */
public function delete($id) { public function delete($id) {
global $wpdb; global $wpdb;
return $wpdb->delete($this->table_name, ['id' => $id]); $result = $wpdb->delete($this->table_name, ['id' => $id]);
// Clear cache
$this->clearCache();
return $result;
}
/**
* Increment usage count
*/
public function incrementUsage($id) {
global $wpdb;
return $wpdb->query(
$wpdb->prepare(
"UPDATE {$this->table_name} SET usage_count = usage_count + 1 WHERE id = %d",
$id
)
);
}
/**
* Clear cache
*/
private function clearCache() {
$this->cache->clearGroup($this->cache_group);
} }
} }

View File

@@ -3,20 +3,24 @@ namespace Sodino\Services;
use Sodino\Repositories\RuleRepository; use Sodino\Repositories\RuleRepository;
use Sodino\Services\TrackingService; use Sodino\Services\TrackingService;
use Sodino\Core\Settings;
use Sodino\Core\Cache;
class PricingService { class PricingService {
private $ruleRepository; private $ruleRepository;
private $trackingService; private $trackingService;
private $rulesCache = null; private $settings;
private $cache;
public function __construct(RuleRepository $ruleRepository, TrackingService $trackingService) { public function __construct(RuleRepository $ruleRepository, TrackingService $trackingService) {
$this->ruleRepository = $ruleRepository; $this->ruleRepository = $ruleRepository;
$this->trackingService = $trackingService; $this->trackingService = $trackingService;
$this->settings = Settings::getInstance();
$this->cache = Cache::getInstance();
} }
public function applyDynamicPricing($price, $product) { public function applyDynamicPricing($price, $product) {
$settings = $this->getSettings(); if (!$this->settings->isPricingEnabled()) {
if (empty($settings['plugin_enabled']) || empty($settings['pricing_enabled'])) {
return $price; return $price;
} }
@@ -25,43 +29,58 @@ class PricingService {
} }
$price = $this->normalizePrice($price); $price = $this->normalizePrice($price);
if (!$settings['cart_pricing_enabled'] && is_cart()) {
if (!$this->settings->get('cart_pricing_enabled') && is_cart()) {
return $price; return $price;
} }
$originalPrice = $price; $originalPrice = $price;
$rules = $this->getEnabledRules(); $rules = $this->getApplicableRules($product);
$matchedRules = [];
foreach ($rules as $rule) { if (empty($rules)) {
if ($this->ruleMatches($rule, $product)) {
$matchedRules[] = $rule;
}
}
if (empty($matchedRules)) {
return $price; return $price;
} }
if (!$settings['allow_multiple_rules']) { if (!$this->settings->get('allow_multiple_rules')) {
$chosenRule = $this->chooseRule($matchedRules, $price, $settings['strategy']); $chosenRule = $this->chooseRule($rules, $price);
$matchedRules = $chosenRule ? [$chosenRule] : []; $rules = $chosenRule ? [$chosenRule] : [];
} }
foreach ($matchedRules as $rule) { foreach ($rules as $rule) {
$oldPrice = $price; $oldPrice = $price;
$price = $this->applyActions($rule, $price); $price = $this->applyRuleActions($rule, $price);
if ($price < $oldPrice) { if ($price < $oldPrice) {
$this->trackingService->recordDiscountApplied($product, $oldPrice, $price, $rule->id); $this->trackingService->recordDiscountApplied($product, $oldPrice, $price, $rule->id);
$this->ruleRepository->incrementUsage($rule->id);
} }
} }
$price = $this->enforceLimits($originalPrice, $price, $settings); $price = $this->enforceLimits($originalPrice, $price);
return max(0, $price); return max(0, $price);
} }
private function chooseRule(array $rules, $price, $strategy) { private function getApplicableRules($product) {
$cache_key = 'applicable_rules_' . ($product ? $product->get_id() : 'all');
return $this->cache->remember($cache_key, function() use ($product) {
$rules = $this->ruleRepository->getEnabled();
$applicable = [];
foreach ($rules as $rule) {
if ($this->ruleMatches($rule, $product)) {
$applicable[] = $rule;
}
}
return $applicable;
}, 300, 'pricing');
}
private function chooseRule(array $rules, $price) {
$strategy = $this->settings->get('strategy', 'priority');
if ($strategy === 'highest_discount') { if ($strategy === 'highest_discount') {
usort($rules, function ($a, $b) use ($price) { usort($rules, function ($a, $b) use ($price) {
return $this->estimateRuleDiscount($b, $price) <=> $this->estimateRuleDiscount($a, $price); return $this->estimateRuleDiscount($b, $price) <=> $this->estimateRuleDiscount($a, $price);
@@ -79,44 +98,19 @@ class PricingService {
return $rules[0] ?? null; return $rules[0] ?? null;
} }
private function getSettings() {
$defaults = [
'plugin_enabled' => 1,
'pricing_enabled' => 1,
'upsell_enabled' => 1,
'allow_multiple_rules' => 0,
'strategy' => 'priority',
'max_discount_percent' => 100,
'min_product_price' => 0,
'ab_testing_enabled' => 0,
'cart_pricing_enabled' => 1,
'scheduled_campaigns_enabled' => 1,
];
return wp_parse_args(get_option('sodino_settings', []), $defaults);
}
private function getEnabledRules() {
if ($this->rulesCache === null) {
$this->rulesCache = $this->ruleRepository->getEnabled();
}
return $this->rulesCache;
}
private function normalizePrice($price) { private function normalizePrice($price) {
if ($price === '' || $price === null) { if ($price === '' || $price === null) {
return 0.0; return 0.0;
} }
return floatval($price); return floatval($price);
} }
private function ruleMatches($rule, $product = null) { private function ruleMatches($rule, $product = null) {
if (!$rule->enabled) { if (!$rule->isActive()) {
return false; return false;
} }
if ($rule->usage_limit > 0 && $this->trackingService->getRuleUsageCount($rule->id) >= $rule->usage_limit) { if ($rule->hasReachedLimit()) {
return false; return false;
} }
@@ -126,10 +120,6 @@ class PricingService {
} }
} }
if (!$this->isRuleActive($rule)) {
return false;
}
if (empty($rule->conditions)) { if (empty($rule->conditions)) {
return true; return true;
} }
@@ -143,20 +133,6 @@ class PricingService {
return true; return true;
} }
private function isRuleActive($rule) {
$now = current_time('Y-m-d H:i:s');
if (!empty($rule->start_date) && $now < $rule->start_date) {
return false;
}
if (!empty($rule->end_date) && $now > $rule->end_date) {
return false;
}
return true;
}
private function evaluateCondition($condition, $product = null) { private function evaluateCondition($condition, $product = null) {
$type = $condition['type'] ?? ''; $type = $condition['type'] ?? '';
$value = $condition['value'] ?? null; $value = $condition['value'] ?? null;
@@ -203,24 +179,21 @@ class PricingService {
return true; return true;
} }
} }
return false; return false;
} }
private function getCartTotal() { private function getCartTotal() {
if (!WC()->cart) { if (!function_exists('WC') || !WC()->cart) {
return 0; return 0;
} }
return floatval(WC()->cart->get_subtotal());
return floatval(WC()->cart->get_cart_contents_total());
} }
private function getCartItemCount() { private function getCartItemCount() {
if (!WC()->cart) { if (!function_exists('WC') || !WC()->cart) {
return 0; return 0;
} }
return intval(WC()->cart->get_cart_contents_count());
return WC()->cart->get_cart_contents_count();
} }
private function productHasCategory($product, $categories) { private function productHasCategory($product, $categories) {
@@ -229,7 +202,11 @@ class PricingService {
} }
$product_cats = wp_get_post_terms($product->get_id(), 'product_cat', ['fields' => 'ids']); $product_cats = wp_get_post_terms($product->get_id(), 'product_cat', ['fields' => 'ids']);
return (bool) array_intersect($product_cats, $categories); if (is_wp_error($product_cats)) {
return false;
}
return (bool) array_intersect($product_cats, array_map('intval', $categories));
} }
private function productIsInIds($product, $ids) { private function productIsInIds($product, $ids) {
@@ -237,14 +214,13 @@ class PricingService {
return false; return false;
} }
return in_array($product->get_id(), $ids, true); return in_array($product->get_id(), array_map('intval', $ids), true);
} }
private function applyActions($rule, $price) { private function applyRuleActions($rule, $price) {
foreach ($rule->actions as $action) { foreach ($rule->actions as $action) {
$price = $this->applyAction($action, $price); $price = $this->applyAction($action, $price);
} }
return $price; return $price;
} }
@@ -263,6 +239,8 @@ class PricingService {
return $price; return $price;
} }
return $price - $value; return $price - $value;
case 'set_price':
return $value > 0 ? $value : $price;
case 'free_shipping': case 'free_shipping':
return $price; return $price;
default: default:
@@ -270,14 +248,15 @@ class PricingService {
} }
} }
private function enforceLimits($originalPrice, $price, array $settings) { private function enforceLimits($originalPrice, $price) {
$minPrice = max(0, floatval($settings['min_product_price'])); $minPrice = max(0, floatval($this->settings->get('min_product_price', 0)));
$price = max($price, $minPrice); $price = max($price, $minPrice);
$maxDiscountPercent = floatval($settings['max_discount_percent']); $maxDiscountPercent = floatval($this->settings->get('max_discount_percent', 100));
if ($maxDiscountPercent > 0 && $maxDiscountPercent < 100) { if ($maxDiscountPercent > 0 && $maxDiscountPercent < 100) {
$limit = $originalPrice * ($maxDiscountPercent / 100); $maxDiscount = $originalPrice * ($maxDiscountPercent / 100);
$price = max($originalPrice - $limit, $price); $minAllowedPrice = $originalPrice - $maxDiscount;
$price = max($minAllowedPrice, $price);
} }
return $price; return $price;

39
composer.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "sodino/wordpress-plugin",
"description": "افزونه هوشمند قیمت‌گذاری و بهینه‌سازی درآمد برای ووکامرس",
"type": "wordpress-plugin",
"license": "GPL-2.0-or-later",
"version": "2.0.0",
"authors": [
{
"name": "Your Name",
"email": "your.email@example.com"
}
],
"require": {
"php": ">=7.4"
},
"require-dev": {
"phpunit/phpunit": "^9.0",
"squizlabs/php_codesniffer": "^3.6"
},
"autoload": {
"psr-4": {
"Sodino\\": "app/"
}
},
"autoload-dev": {
"psr-4": {
"Sodino\\Tests\\": "tests/"
}
},
"scripts": {
"test": "phpunit",
"phpcs": "phpcs --standard=WordPress app/",
"phpcbf": "phpcbf --standard=WordPress app/"
},
"config": {
"optimize-autoloader": true,
"sort-packages": true
}
}

View File

@@ -7,11 +7,11 @@ if (!defined('ABSPATH')) {
/** /**
* Database migrations for Sodino plugin * Database migrations for Sodino plugin
*/ */
function sodino_create_tables() { function sodino_create_tables() {
global $wpdb; global $wpdb;
$charset_collate = $wpdb->get_charset_collate(); $charset_collate = $wpdb->get_charset_collate();
$current_version = get_option('sodino_db_version', '0');
// Rules table // Rules table
$rules_table = $wpdb->prefix . 'sodino_rules'; $rules_table = $wpdb->prefix . 'sodino_rules';
@@ -22,23 +22,22 @@ function sodino_create_tables() {
actions longtext NOT NULL, actions longtext NOT NULL,
priority int(11) NOT NULL DEFAULT 10, priority int(11) NOT NULL DEFAULT 10,
usage_limit int(11) NOT NULL DEFAULT 0, usage_limit int(11) NOT NULL DEFAULT 0,
usage_count int(11) NOT NULL DEFAULT 0,
user_roles varchar(255) DEFAULT '', user_roles varchar(255) DEFAULT '',
start_date datetime NULL, start_date datetime NULL,
end_date datetime NULL, end_date datetime NULL,
enabled tinyint(1) DEFAULT 1, enabled tinyint(1) DEFAULT 1,
condition_type varchar(100) DEFAULT NULL,
condition_value varchar(255) DEFAULT NULL,
action_type varchar(100) DEFAULT NULL,
action_value varchar(255) DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP, created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id) PRIMARY KEY (id),
KEY enabled_priority (enabled, priority),
KEY start_end_dates (start_date, end_date)
) $charset_collate;"; ) $charset_collate;";
// Events table // Events table
$events_table = $wpdb->prefix . 'sodino_events'; $events_table = $wpdb->prefix . 'sodino_events';
$events_sql = "CREATE TABLE $events_table ( $events_sql = "CREATE TABLE $events_table (
id mediumint(9) NOT NULL AUTO_INCREMENT, id bigint(20) NOT NULL AUTO_INCREMENT,
event_type varchar(100) NOT NULL, event_type varchar(100) NOT NULL,
product_id mediumint(9) DEFAULT NULL, product_id mediumint(9) DEFAULT NULL,
variation_id mediumint(9) DEFAULT NULL, variation_id mediumint(9) DEFAULT NULL,
@@ -49,7 +48,12 @@ function sodino_create_tables() {
discount_value decimal(10,2) DEFAULT 0, discount_value decimal(10,2) DEFAULT 0,
metadata longtext DEFAULT NULL, metadata longtext DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP, created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id) PRIMARY KEY (id),
KEY event_type_created (event_type, created_at),
KEY product_id (product_id),
KEY rule_id (rule_id),
KEY session_id (session_id),
KEY created_at (created_at)
) $charset_collate;"; ) $charset_collate;";
// Upsell table // Upsell table
@@ -64,9 +68,13 @@ function sodino_create_tables() {
discount_value decimal(10,2) DEFAULT 0, discount_value decimal(10,2) DEFAULT 0,
status tinyint(1) DEFAULT 1, status tinyint(1) DEFAULT 1,
priority int(11) NOT NULL DEFAULT 10, priority int(11) NOT NULL DEFAULT 10,
impressions bigint(20) NOT NULL DEFAULT 0,
conversions bigint(20) NOT NULL DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP, created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id) PRIMARY KEY (id),
KEY status_priority (status, priority),
KEY trigger_type (trigger_type)
) $charset_collate;"; ) $charset_collate;";
// Banner table // Banner table
@@ -88,7 +96,25 @@ function sodino_create_tables() {
impressions bigint(20) NOT NULL DEFAULT 0, impressions bigint(20) NOT NULL DEFAULT 0,
clicks bigint(20) NOT NULL DEFAULT 0, clicks bigint(20) NOT NULL DEFAULT 0,
created_at datetime DEFAULT CURRENT_TIMESTAMP, created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id) updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY status_priority (status, priority),
KEY position (position),
KEY start_end_time (start_time, end_time)
) $charset_collate;";
// Analytics cache table
$analytics_table = $wpdb->prefix . 'sodino_analytics_cache';
$analytics_sql = "CREATE TABLE $analytics_table (
id bigint(20) NOT NULL AUTO_INCREMENT,
cache_key varchar(255) NOT NULL,
cache_value longtext NOT NULL,
cache_group varchar(100) NOT NULL DEFAULT 'general',
expires_at datetime NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY cache_key_group (cache_key, cache_group),
KEY expires_at (expires_at)
) $charset_collate;"; ) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
@@ -96,7 +122,42 @@ function sodino_create_tables() {
dbDelta($events_sql); dbDelta($events_sql);
dbDelta($upsell_sql); dbDelta($upsell_sql);
dbDelta($banner_sql); dbDelta($banner_sql);
dbDelta($analytics_sql);
// Run migrations
sodino_run_migrations($current_version);
// Add version option // Add version option
update_option('sodino_db_version', '1.3'); update_option('sodino_db_version', '2.0');
}
/**
* Run incremental migrations
*/
function sodino_run_migrations($from_version) {
global $wpdb;
// Migration from 1.x to 2.0
if (version_compare($from_version, '2.0', '<')) {
// Add usage_count column if not exists
$rules_table = $wpdb->prefix . 'sodino_rules';
$column_exists = $wpdb->get_results("SHOW COLUMNS FROM {$rules_table} LIKE 'usage_count'");
if (empty($column_exists)) {
$wpdb->query("ALTER TABLE {$rules_table} ADD COLUMN usage_count int(11) NOT NULL DEFAULT 0 AFTER usage_limit");
}
// Remove deprecated columns
$deprecated_columns = ['condition_type', 'condition_value', 'action_type', 'action_value'];
foreach ($deprecated_columns as $col) {
$col_exists = $wpdb->get_results("SHOW COLUMNS FROM {$rules_table} LIKE '{$col}'");
if (!empty($col_exists)) {
$wpdb->query("ALTER TABLE {$rules_table} DROP COLUMN {$col}");
}
}
// Add indexes for better performance
$wpdb->query("ALTER TABLE {$rules_table} ADD INDEX enabled_priority (enabled, priority)");
$wpdb->query("ALTER TABLE {$rules_table} ADD INDEX start_end_dates (start_date, end_date)");
}
} }

View File

@@ -1,5 +1,5 @@
=== Sodino === === Sodino ===
Contributors: yourname 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.0

View File

@@ -3,7 +3,7 @@
* Plugin Name: Sodino (سودینو) * Plugin Name: Sodino (سودینو)
* Plugin URI: https://example.com/sodino * Plugin URI: https://example.com/sodino
* Description: افزونه هوشمند قیمت‌گذاری و بهینه‌سازی درآمد برای ووکامرس. قیمت محصولات را بر اساس رفتار کاربر و قوانین تعریف‌شده به صورت پویا تنظیم می‌کند. * Description: افزونه هوشمند قیمت‌گذاری و بهینه‌سازی درآمد برای ووکامرس. قیمت محصولات را بر اساس رفتار کاربر و قوانین تعریف‌شده به صورت پویا تنظیم می‌کند.
* Version: 1.0.0 * Version: 2.0.0
* Author: Your Name * Author: Your Name
* License: GPL v2 or later * License: GPL v2 or later
* Text Domain: sodino * Text Domain: sodino
@@ -20,7 +20,7 @@ if (!defined('ABSPATH')) {
} }
// Define plugin constants // Define plugin constants
define('SODINO_VERSION', '1.0.0'); define('SODINO_VERSION', '2.0.0');
define('SODINO_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('SODINO_PLUGIN_DIR', plugin_dir_path(__FILE__));
define('SODINO_PLUGIN_URL', plugin_dir_url(__FILE__)); define('SODINO_PLUGIN_URL', plugin_dir_url(__FILE__));
define('SODINO_PLUGIN_BASENAME', plugin_basename(__FILE__)); define('SODINO_PLUGIN_BASENAME', plugin_basename(__FILE__));
@@ -60,6 +60,18 @@ function sodino_activate() {
// Flush rewrite rules if needed // Flush rewrite rules if needed
flush_rewrite_rules(); flush_rewrite_rules();
// Set default settings
if (!get_option('sodino_settings')) {
update_option('sodino_settings', [
'plugin_enabled' => 1,
'pricing_enabled' => 1,
'upsell_enabled' => 1,
'banner_enabled' => 1,
'cache_enabled' => 1,
'cache_duration' => 3600,
]);
}
} }
// Deactivation hook // Deactivation hook
@@ -70,6 +82,10 @@ function sodino_deactivate() {
// Clear analytics cron // Clear analytics cron
wp_clear_scheduled_hook('sodino_hourly_analytics'); wp_clear_scheduled_hook('sodino_hourly_analytics');
// Clear all cache
$cache = \Sodino\Core\Cache::getInstance();
$cache->clearAll();
} }
// Bootstrap the plugin // Bootstrap the plugin
@@ -80,25 +96,44 @@ function sodino_init() {
return; return;
} }
// Load text domain
load_plugin_textdomain('sodino', false, dirname(SODINO_PLUGIN_BASENAME) . '/languages/');
// Initialize admin // Initialize admin
if (is_admin()) { if (is_admin()) {
require_once SODINO_PLUGIN_DIR . 'admin/admin.php'; require_once SODINO_PLUGIN_DIR . 'admin/admin.php';
} }
// Initialize public hooks // Initialize public hooks
require_once SODINO_PLUGIN_DIR . 'public/hooks/pricing-hooks.php'; sodino_init_public_hooks();
require_once SODINO_PLUGIN_DIR . 'public/hooks/analytics-hooks.php';
require_once SODINO_PLUGIN_DIR . 'public/hooks/upsell-hooks.php';
require_once SODINO_PLUGIN_DIR . 'public/hooks/banner-hooks.php';
// Schedule analytics aggregation if needed // Schedule analytics aggregation if needed
sodino_schedule_analytics(); sodino_schedule_analytics();
// Load text domain
load_plugin_textdomain('sodino', false, dirname(SODINO_PLUGIN_BASENAME) . '/languages/');
} }
add_action('plugins_loaded', 'sodino_init'); add_action('plugins_loaded', 'sodino_init');
/**
* Initialize public hooks
*/
function sodino_init_public_hooks() {
$settings = \Sodino\Core\Settings::getInstance();
if ($settings->isPricingEnabled()) {
require_once SODINO_PLUGIN_DIR . 'public/hooks/pricing-hooks.php';
}
if ($settings->isUpsellEnabled()) {
require_once SODINO_PLUGIN_DIR . 'public/hooks/upsell-hooks.php';
}
if ($settings->isBannerEnabled()) {
require_once SODINO_PLUGIN_DIR . 'public/hooks/banner-hooks.php';
}
// Always load analytics
require_once SODINO_PLUGIN_DIR . 'public/hooks/analytics-hooks.php';
}
/** /**
* Schedule analytics cron job * Schedule analytics cron job
*/ */
@@ -131,3 +166,12 @@ function sodino_woocommerce_missing_notice() {
</div> </div>
<?php <?php
} }
/**
* Add settings link on plugin page
*/
add_filter('plugin_action_links_' . SODINO_PLUGIN_BASENAME, function($links) {
$settings_link = '<a href="' . admin_url('admin.php?page=sodino-settings') . '">' . __('تنظیمات', 'sodino') . '</a>';
array_unshift($links, $settings_link);
return $links;
});