-
-
-
+// Load components
+require_once SODINO_PLUGIN_DIR . 'admin/components/layout.php';
+
+sodino_admin_layout($current_page ?? 'sodino-settings', function() use ($settings) {
+ ?>
+
+
+
+
+
-
-
-
-
-
-
+
diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php
new file mode 100644
index 0000000..1d90048
--- /dev/null
+++ b/app/Controllers/BaseController.php
@@ -0,0 +1,94 @@
+ $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(
+ '
',
+ esc_attr($class),
+ esc_html($notice['message'])
+ );
+ delete_transient('sodino_admin_notice');
+ }
+ }
+}
diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php
new file mode 100644
index 0000000..7387005
--- /dev/null
+++ b/app/Controllers/DashboardController.php
@@ -0,0 +1,48 @@
+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'
+ ]);
+ }
+}
diff --git a/app/Controllers/RuleController.php b/app/Controllers/RuleController.php
new file mode 100644
index 0000000..01c5103
--- /dev/null
+++ b/app/Controllers/RuleController.php
@@ -0,0 +1,189 @@
+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']);
+ }
+ }
+}
diff --git a/app/Controllers/SettingsController.php b/app/Controllers/SettingsController.php
new file mode 100644
index 0000000..1dacb27
--- /dev/null
+++ b/app/Controllers/SettingsController.php
@@ -0,0 +1,87 @@
+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')
+ );
+ }
+}
diff --git a/app/Core/Cache.php b/app/Core/Cache.php
new file mode 100644
index 0000000..66cf705
--- /dev/null
+++ b/app/Core/Cache.php
@@ -0,0 +1,137 @@
+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}";
+ }
+}
diff --git a/app/Core/Settings.php b/app/Core/Settings.php
new file mode 100644
index 0000000..5fe6748
--- /dev/null
+++ b/app/Core/Settings.php
@@ -0,0 +1,125 @@
+ 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');
+ }
+}
diff --git a/app/Core/Validator.php b/app/Core/Validator.php
new file mode 100644
index 0000000..c0bacb7
--- /dev/null
+++ b/app/Core/Validator.php
@@ -0,0 +1,132 @@
+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);
+ }
+}
diff --git a/app/Models/Rule.php b/app/Models/Rule.php
index aa6cc91..58f9a90 100644
--- a/app/Models/Rule.php
+++ b/app/Models/Rule.php
@@ -11,14 +11,11 @@ class Rule {
public $actions;
public $priority;
public $usage_limit;
+ public $usage_count;
public $user_roles;
public $start_date;
public $end_date;
public $enabled;
- public $condition_type;
- public $condition_value;
- public $action_type;
- public $action_value;
public $created_at;
public $updated_at;
@@ -32,28 +29,13 @@ class Rule {
$this->actions = $this->parseJsonField($data['actions'] ?? '[]');
$this->priority = isset($data['priority']) ? (int) $data['priority'] : 10;
$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->start_date = $data['start_date'] ?? null;
$this->end_date = $data['end_date'] ?? null;
$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->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) {
@@ -88,16 +70,41 @@ class Rule {
'actions' => wp_json_encode($this->actions),
'priority' => $this->priority,
'usage_limit' => $this->usage_limit,
+ 'usage_count' => $this->usage_count,
'user_roles' => is_array($this->user_roles) ? implode(',', $this->user_roles) : $this->user_roles,
'start_date' => $this->start_date,
'end_date' => $this->end_date,
'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,
'updated_at' => $this->updated_at,
];
}
-}
\ No newline at end of file
+
+ /**
+ * 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;
+ }
+}
diff --git a/app/Repositories/RuleRepository.php b/app/Repositories/RuleRepository.php
index 2bf1eef..6a76af3 100644
--- a/app/Repositories/RuleRepository.php
+++ b/app/Repositories/RuleRepository.php
@@ -2,51 +2,80 @@
namespace Sodino\Repositories;
use Sodino\Models\Rule;
+use Sodino\Core\Cache;
/**
* Rule Repository
*/
class RuleRepository {
private $table_name;
+ private $cache;
+ private $cache_group = 'rules';
+ private $cache_duration = 3600;
public function __construct() {
global $wpdb;
$this->table_name = $wpdb->prefix . 'sodino_rules';
+ $this->cache = Cache::getInstance();
}
/**
* Get all rules
*/
public function getAll() {
- global $wpdb;
- $results = $wpdb->get_results("SELECT * FROM {$this->table_name} ORDER BY priority DESC, id ASC", ARRAY_A);
- $rules = [];
- foreach ($results as $result) {
- $rules[] = new Rule($result);
- }
- return $rules;
+ return $this->cache->remember('all_rules', function() {
+ global $wpdb;
+ $results = $wpdb->get_results(
+ "SELECT * FROM {$this->table_name} ORDER BY priority DESC, id ASC",
+ ARRAY_A
+ );
+ $rules = [];
+ foreach ($results as $result) {
+ $rules[] = new Rule($result);
+ }
+ return $rules;
+ }, $this->cache_duration, $this->cache_group);
}
/**
* Get rule by ID
*/
public function getById($id) {
- global $wpdb;
- $result = $wpdb->get_row($wpdb->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id), ARRAY_A);
- return $result ? new Rule($result) : null;
+ return $this->cache->remember("rule_{$id}", function() use ($id) {
+ global $wpdb;
+ $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
*/
public function getEnabled() {
- global $wpdb;
- $results = $wpdb->get_results("SELECT * FROM {$this->table_name} WHERE enabled = 1 ORDER BY priority DESC, id ASC", ARRAY_A);
- $rules = [];
- foreach ($results as $result) {
- $rules[] = new Rule($result);
- }
- return $rules;
+ return $this->cache->remember('enabled_rules', function() {
+ global $wpdb;
+ $now = current_time('mysql');
+ $results = $wpdb->get_results(
+ $wpdb->prepare(
+ "SELECT * FROM {$this->table_name}
+ 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) {
$wpdb->update($this->table_name, $data, ['id' => $rule->id]);
- return $rule->id;
+ $id = $rule->id;
} else {
$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) {
global $wpdb;
- return $wpdb->delete($this->table_name, ['id' => $id]);
+ $result = $wpdb->delete($this->table_name, ['id' => $id]);
+
+ // Clear cache
+ $this->clearCache();
+
+ return $result;
}
-}
\ No newline at end of file
+
+ /**
+ * 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);
+ }
+}
diff --git a/app/Services/PricingService.php b/app/Services/PricingService.php
index 782dfcf..68a0096 100644
--- a/app/Services/PricingService.php
+++ b/app/Services/PricingService.php
@@ -3,20 +3,24 @@ namespace Sodino\Services;
use Sodino\Repositories\RuleRepository;
use Sodino\Services\TrackingService;
+use Sodino\Core\Settings;
+use Sodino\Core\Cache;
class PricingService {
private $ruleRepository;
private $trackingService;
- private $rulesCache = null;
+ private $settings;
+ private $cache;
public function __construct(RuleRepository $ruleRepository, TrackingService $trackingService) {
$this->ruleRepository = $ruleRepository;
$this->trackingService = $trackingService;
+ $this->settings = Settings::getInstance();
+ $this->cache = Cache::getInstance();
}
public function applyDynamicPricing($price, $product) {
- $settings = $this->getSettings();
- if (empty($settings['plugin_enabled']) || empty($settings['pricing_enabled'])) {
+ if (!$this->settings->isPricingEnabled()) {
return $price;
}
@@ -25,43 +29,58 @@ class PricingService {
}
$price = $this->normalizePrice($price);
- if (!$settings['cart_pricing_enabled'] && is_cart()) {
+
+ if (!$this->settings->get('cart_pricing_enabled') && is_cart()) {
return $price;
}
$originalPrice = $price;
- $rules = $this->getEnabledRules();
- $matchedRules = [];
+ $rules = $this->getApplicableRules($product);
- foreach ($rules as $rule) {
- if ($this->ruleMatches($rule, $product)) {
- $matchedRules[] = $rule;
- }
- }
-
- if (empty($matchedRules)) {
+ if (empty($rules)) {
return $price;
}
- if (!$settings['allow_multiple_rules']) {
- $chosenRule = $this->chooseRule($matchedRules, $price, $settings['strategy']);
- $matchedRules = $chosenRule ? [$chosenRule] : [];
+ if (!$this->settings->get('allow_multiple_rules')) {
+ $chosenRule = $this->chooseRule($rules, $price);
+ $rules = $chosenRule ? [$chosenRule] : [];
}
- foreach ($matchedRules as $rule) {
+ foreach ($rules as $rule) {
$oldPrice = $price;
- $price = $this->applyActions($rule, $price);
+ $price = $this->applyRuleActions($rule, $price);
+
if ($price < $oldPrice) {
$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);
}
- 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') {
usort($rules, function ($a, $b) use ($price) {
return $this->estimateRuleDiscount($b, $price) <=> $this->estimateRuleDiscount($a, $price);
@@ -79,44 +98,19 @@ class PricingService {
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) {
if ($price === '' || $price === null) {
return 0.0;
}
-
return floatval($price);
}
private function ruleMatches($rule, $product = null) {
- if (!$rule->enabled) {
+ if (!$rule->isActive()) {
return false;
}
- if ($rule->usage_limit > 0 && $this->trackingService->getRuleUsageCount($rule->id) >= $rule->usage_limit) {
+ if ($rule->hasReachedLimit()) {
return false;
}
@@ -126,10 +120,6 @@ class PricingService {
}
}
- if (!$this->isRuleActive($rule)) {
- return false;
- }
-
if (empty($rule->conditions)) {
return true;
}
@@ -143,20 +133,6 @@ class PricingService {
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) {
$type = $condition['type'] ?? '';
$value = $condition['value'] ?? null;
@@ -203,24 +179,21 @@ class PricingService {
return true;
}
}
-
return false;
}
private function getCartTotal() {
- if (!WC()->cart) {
+ if (!function_exists('WC') || !WC()->cart) {
return 0;
}
-
- return floatval(WC()->cart->get_cart_contents_total());
+ return floatval(WC()->cart->get_subtotal());
}
private function getCartItemCount() {
- if (!WC()->cart) {
+ if (!function_exists('WC') || !WC()->cart) {
return 0;
}
-
- return WC()->cart->get_cart_contents_count();
+ return intval(WC()->cart->get_cart_contents_count());
}
private function productHasCategory($product, $categories) {
@@ -229,7 +202,11 @@ class PricingService {
}
$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) {
@@ -237,14 +214,13 @@ class PricingService {
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) {
$price = $this->applyAction($action, $price);
}
-
return $price;
}
@@ -263,6 +239,8 @@ class PricingService {
return $price;
}
return $price - $value;
+ case 'set_price':
+ return $value > 0 ? $value : $price;
case 'free_shipping':
return $price;
default:
@@ -270,14 +248,15 @@ class PricingService {
}
}
- private function enforceLimits($originalPrice, $price, array $settings) {
- $minPrice = max(0, floatval($settings['min_product_price']));
+ private function enforceLimits($originalPrice, $price) {
+ $minPrice = max(0, floatval($this->settings->get('min_product_price', 0)));
$price = max($price, $minPrice);
- $maxDiscountPercent = floatval($settings['max_discount_percent']);
+ $maxDiscountPercent = floatval($this->settings->get('max_discount_percent', 100));
if ($maxDiscountPercent > 0 && $maxDiscountPercent < 100) {
- $limit = $originalPrice * ($maxDiscountPercent / 100);
- $price = max($originalPrice - $limit, $price);
+ $maxDiscount = $originalPrice * ($maxDiscountPercent / 100);
+ $minAllowedPrice = $originalPrice - $maxDiscount;
+ $price = max($minAllowedPrice, $price);
}
return $price;
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..fe86e0c
--- /dev/null
+++ b/composer.json
@@ -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
+ }
+}
diff --git a/database/migrations.php b/database/migrations.php
index eada823..4ba1201 100644
--- a/database/migrations.php
+++ b/database/migrations.php
@@ -7,11 +7,11 @@ if (!defined('ABSPATH')) {
/**
* Database migrations for Sodino plugin
*/
-
function sodino_create_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
+ $current_version = get_option('sodino_db_version', '0');
// Rules table
$rules_table = $wpdb->prefix . 'sodino_rules';
@@ -22,23 +22,22 @@ function sodino_create_tables() {
actions longtext NOT NULL,
priority int(11) NOT NULL DEFAULT 10,
usage_limit int(11) NOT NULL DEFAULT 0,
+ usage_count int(11) NOT NULL DEFAULT 0,
user_roles varchar(255) DEFAULT '',
start_date datetime NULL,
end_date datetime NULL,
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,
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;";
// Events table
$events_table = $wpdb->prefix . 'sodino_events';
$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,
product_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,
metadata longtext DEFAULT NULL,
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;";
// Upsell table
@@ -64,9 +68,13 @@ function sodino_create_tables() {
discount_value decimal(10,2) DEFAULT 0,
status tinyint(1) DEFAULT 1,
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,
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;";
// Banner table
@@ -88,7 +96,25 @@ function sodino_create_tables() {
impressions bigint(20) NOT NULL DEFAULT 0,
clicks bigint(20) NOT NULL DEFAULT 0,
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;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
@@ -96,7 +122,42 @@ function sodino_create_tables() {
dbDelta($events_sql);
dbDelta($upsell_sql);
dbDelta($banner_sql);
+ dbDelta($analytics_sql);
+
+ // Run migrations
+ sodino_run_migrations($current_version);
// Add version option
- update_option('sodino_db_version', '1.3');
-}
\ No newline at end of file
+ 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)");
+ }
+}
diff --git a/readme.txt b/readme.txt
index 3b5d4b9..44f741f 100644
--- a/readme.txt
+++ b/readme.txt
@@ -1,5 +1,5 @@
=== Sodino ===
-Contributors: yourname
+Contributors: Soheil khaledabadi
Tags: woocommerce, pricing, dynamic pricing, revenue optimization
Requires at least: 5.0
Tested up to: 6.0
diff --git a/sodino.php b/sodino.php
index 50d9366..d058a1d 100644
--- a/sodino.php
+++ b/sodino.php
@@ -3,7 +3,7 @@
* Plugin Name: Sodino (سودینو)
* Plugin URI: https://example.com/sodino
* Description: افزونه هوشمند قیمتگذاری و بهینهسازی درآمد برای ووکامرس. قیمت محصولات را بر اساس رفتار کاربر و قوانین تعریفشده به صورت پویا تنظیم میکند.
- * Version: 1.0.0
+ * Version: 2.0.0
* Author: Your Name
* License: GPL v2 or later
* Text Domain: sodino
@@ -20,7 +20,7 @@ if (!defined('ABSPATH')) {
}
// 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_URL', plugin_dir_url(__FILE__));
define('SODINO_PLUGIN_BASENAME', plugin_basename(__FILE__));
@@ -60,6 +60,18 @@ function sodino_activate() {
// Flush rewrite rules if needed
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
@@ -70,6 +82,10 @@ function sodino_deactivate() {
// Clear analytics cron
wp_clear_scheduled_hook('sodino_hourly_analytics');
+
+ // Clear all cache
+ $cache = \Sodino\Core\Cache::getInstance();
+ $cache->clearAll();
}
// Bootstrap the plugin
@@ -79,6 +95,9 @@ function sodino_init() {
add_action('admin_notices', 'sodino_woocommerce_missing_notice');
return;
}
+
+ // Load text domain
+ load_plugin_textdomain('sodino', false, dirname(SODINO_PLUGIN_BASENAME) . '/languages/');
// Initialize admin
if (is_admin()) {
@@ -86,19 +105,35 @@ function sodino_init() {
}
// Initialize public hooks
- require_once SODINO_PLUGIN_DIR . 'public/hooks/pricing-hooks.php';
- 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';
+ sodino_init_public_hooks();
// Schedule analytics aggregation if needed
sodino_schedule_analytics();
-
- // Load text domain
- load_plugin_textdomain('sodino', false, dirname(SODINO_PLUGIN_BASENAME) . '/languages/');
}
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
*/
@@ -130,4 +165,13 @@ function sodino_woocommerce_missing_notice() {
' . __('تنظیمات', 'sodino') . '';
+ array_unshift($links, $settings_link);
+ return $links;
+});