From fd9d29a0eed015367ff78a221b75b682a1dd8aeb Mon Sep 17 00:00:00 2001 From: soheil khaledabadi Date: Fri, 8 May 2026 19:16:01 +0330 Subject: [PATCH] feat(upsell): apply real cart discounts and track performance --- admin/admin.php | 13 +- admin/class-upsell-list-table.php | 17 ++ admin/components/sidebar.php | 2 +- admin/views/banner-form.php | 4 +- admin/views/banner-list.php | 4 +- admin/views/competitor-price.php | 152 ------------------ admin/views/dashboard.php | 4 +- admin/views/rule-form.php | 4 +- admin/views/rules-list.php | 4 +- admin/views/tools.php | 131 ++++++++++++++++ admin/views/upsell-form.php | 4 +- admin/views/upsell-list.php | 4 +- app/Controllers/AdminController.php | 130 +++++++++++++++- app/Models/Upsell.php | 6 + app/Repositories/UpsellRepository.php | 14 ++ app/Services/UpsellService.php | 37 ++++- database/migrations.php | 11 ++ public/css/upsell-frontend.css | 213 ++++++++++++++++++++++++++ public/hooks/upsell-hooks.php | 199 ++++++++++++++++++++---- 19 files changed, 747 insertions(+), 206 deletions(-) delete mode 100644 admin/views/competitor-price.php create mode 100644 admin/views/tools.php create mode 100644 public/css/upsell-frontend.css diff --git a/admin/admin.php b/admin/admin.php index 222119e..7356d16 100644 --- a/admin/admin.php +++ b/admin/admin.php @@ -184,11 +184,11 @@ add_action('admin_menu', function() use ($adminController) { add_submenu_page( 'sodino-dashboard', - __('قیمت رقبا (به‌زودی)', 'sodino'), - __('قیمت رقبا (به‌زودی)', 'sodino'), + __('ابزارها و سلامت', 'sodino'), + __('ابزارها و سلامت', 'sodino'), 'manage_options', - 'sodino-competitor-price', - [$adminController, 'competitorPricePage'] + 'sodino-tools', + [$adminController, 'toolsPage'] ); add_submenu_page( @@ -255,6 +255,11 @@ add_action('admin_init', function() use ($ruleController, $settingsController, $ $settingsController->clearCache(); } + // Tools actions + if ($page === 'sodino-tools') { + $adminController->handleToolsActions(); + } + // Banner actions if (strpos($page, 'sodino') === 0 && in_array($action, ['delete_banner', 'toggle_banner_status'], true)) { $adminController->handleBannerActions(); diff --git a/admin/class-upsell-list-table.php b/admin/class-upsell-list-table.php index c1fe645..0a76a1c 100644 --- a/admin/class-upsell-list-table.php +++ b/admin/class-upsell-list-table.php @@ -28,6 +28,7 @@ class Sodino_Upsell_List_Table extends WP_List_Table { 'trigger' => __('شرط فعال‌سازی', 'sodino'), 'suggested_product'=> __('محصول پیشنهادی', 'sodino'), 'discount' => __('تخفیف', 'sodino'), + 'performance' => __('عملکرد', 'sodino'), 'status' => __('وضعیت', 'sodino'), 'actions' => __('عملیات', 'sodino'), ]; @@ -86,6 +87,22 @@ class Sodino_Upsell_List_Table extends WP_List_Table { return __('بدون تخفیف', 'sodino'); } + public function column_performance($item) { + $impressions = max(0, (int) ($item->impressions ?? 0)); + $conversions = max(0, (int) ($item->conversions ?? 0)); + $rate = $impressions > 0 ? round(($conversions / $impressions) * 100, 2) : 0; + + return sprintf( + '%s: %s
%s: %s
%s: %s%%', + esc_html__('نمایش', 'sodino'), + esc_html(number_format_i18n($impressions)), + esc_html__('افزودن', 'sodino'), + esc_html(number_format_i18n($conversions)), + esc_html__('نرخ', 'sodino'), + esc_html(number_format_i18n($rate, 2)) + ); + } + public function column_status($item) { return $item->status ? __('فعال', 'sodino') : __('غیرفعال', 'sodino'); } diff --git a/admin/components/sidebar.php b/admin/components/sidebar.php index b4c4b27..19d9584 100644 --- a/admin/components/sidebar.php +++ b/admin/components/sidebar.php @@ -13,7 +13,7 @@ $menu_items = [ 'sodino-add-upsell' => __('افزودن آپسل', 'sodino'), 'sodino-banners' => __('بنرهای هوشمند', 'sodino'), 'sodino-add-banner' => __('افزودن بنر', 'sodino'), - 'sodino-competitor-price' => __('قیمت رقبا (به‌زودی)', 'sodino'), + 'sodino-tools' => __('ابزارها و سلامت', 'sodino'), 'sodino-settings' => __('تنظیمات', 'sodino'), ]; ?> diff --git a/admin/views/banner-form.php b/admin/views/banner-form.php index 3028dfb..2c370e1 100644 --- a/admin/views/banner-form.php +++ b/admin/views/banner-form.php @@ -50,8 +50,8 @@ $form_display_type = function_exists('sodino_old_input') ? sodino_old_input('dis - - + + diff --git a/admin/views/banner-list.php b/admin/views/banner-list.php index 2051f47..3b4aa18 100644 --- a/admin/views/banner-list.php +++ b/admin/views/banner-list.php @@ -48,8 +48,8 @@ $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-banners'); - - + + diff --git a/admin/views/competitor-price.php b/admin/views/competitor-price.php deleted file mode 100644 index 5e68ab1..0000000 --- a/admin/views/competitor-price.php +++ /dev/null @@ -1,152 +0,0 @@ - -
-
-
-
-
-
-

-

-
-
-
-
-
- -
- -
- - -
-
-
-
-

-

-
- -
-
- -
-
-

-

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
23,500 تومان24,900 تومان1,400 تومان کمتر
78,000 تومان82,000 تومان4,000 تومان کمتر
155,000 تومان161,000 تومان6,000 تومان بیشتر
-
-
- -
-
-
-

- -
-
-
- - -
-
- - -
- -
-
-
-
-
-

-

-
-
- -
-
-
- -
-
-
-
-
-
-
-
diff --git a/admin/views/dashboard.php b/admin/views/dashboard.php index 2bd0fb6..29ca79d 100644 --- a/admin/views/dashboard.php +++ b/admin/views/dashboard.php @@ -55,8 +55,8 @@ $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-dashboard'); - - + + diff --git a/admin/views/rule-form.php b/admin/views/rule-form.php index bc7ab3c..f362d78 100644 --- a/admin/views/rule-form.php +++ b/admin/views/rule-form.php @@ -52,8 +52,8 @@ $form_action_type = function_exists('sodino_old_input') ? sodino_old_input('acti - - + + diff --git a/admin/views/rules-list.php b/admin/views/rules-list.php index 9889e6c..d1848a0 100644 --- a/admin/views/rules-list.php +++ b/admin/views/rules-list.php @@ -50,8 +50,8 @@ $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-rules'); - - + + diff --git a/admin/views/tools.php b/admin/views/tools.php new file mode 100644 index 0000000..1026a9d --- /dev/null +++ b/admin/views/tools.php @@ -0,0 +1,131 @@ + +
+
+
+
+

+

+
+
+
+ +
+ +
+ + +
+ + +
+
+

+
+ + =')) : ?> + + + + +
+
+ +
+

+ +

+ +

+
+ +
+

+
+ + + +
+
+
+ +
+
+

+
+
+ + + + + + + + + + + + + + + + + + + +
+ + + + + +
+
+
+ +
+
+
+

+

+ +

+
+ + + +
+
+
+
+
+
diff --git a/admin/views/upsell-form.php b/admin/views/upsell-form.php index d38e955..45b41e8 100644 --- a/admin/views/upsell-form.php +++ b/admin/views/upsell-form.php @@ -69,8 +69,8 @@ $product_categories = get_terms([ - - + + diff --git a/admin/views/upsell-list.php b/admin/views/upsell-list.php index d90a55a..9ec68f8 100644 --- a/admin/views/upsell-list.php +++ b/admin/views/upsell-list.php @@ -48,8 +48,8 @@ $current_page = sanitize_text_field($_GET['page'] ?? 'sodino-upsells'); - - + + diff --git a/app/Controllers/AdminController.php b/app/Controllers/AdminController.php index a4d301b..1e6492e 100644 --- a/app/Controllers/AdminController.php +++ b/app/Controllers/AdminController.php @@ -102,11 +102,11 @@ class AdminController { add_submenu_page( 'sodino-rules', - __('قیمت رقبا (به‌زودی)', 'sodino'), - __('قیمت رقبا (به‌زودی)', 'sodino'), + __('ابزارها و سلامت', 'sodino'), + __('ابزارها و سلامت', 'sodino'), 'manage_options', - 'sodino-competitor-price', - [$this, 'competitorPricePage'] + 'sodino-tools', + [$this, 'toolsPage'] ); add_submenu_page( @@ -295,10 +295,49 @@ class AdminController { } /** - * Competitor price page + * Tools and health page */ - public function competitorPricePage() { - include SODINO_PLUGIN_DIR . 'admin/views/competitor-price.php'; + public function toolsPage() { + $toolsData = $this->getToolsData(); + include SODINO_PLUGIN_DIR . 'admin/views/tools.php'; + } + + public function handleToolsActions() { + if (!current_user_can('manage_options')) { + return; + } + + if (($_GET['page'] ?? '') !== 'sodino-tools' || empty($_GET['tool_action'])) { + return; + } + + $action = sanitize_key($_GET['tool_action']); + if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], 'sodino_tools_' . $action)) { + wp_die(__('خطای امنیتی رخ داد.', 'sodino')); + } + + if ($action === 'clear_cache') { + \Sodino\Core\Cache::getInstance()->clearAll(); + $this->redirectWithNotice(admin_url('admin.php?page=sodino-tools'), __('کش سودینو با موفقیت پاک شد.', 'sodino'), 'success'); + } + + if ($action === 'run_migrations') { + require_once SODINO_PLUGIN_DIR . 'database/migrations.php'; + sodino_create_tables(); + $this->redirectWithNotice(admin_url('admin.php?page=sodino-tools'), __('ساختار دیتابیس سودینو بررسی و به‌روزرسانی شد.', 'sodino'), 'success'); + } + + if ($action === 'prune_events') { + $deleted = $this->deleteOldEvents(90); + $this->redirectWithNotice( + admin_url('admin.php?page=sodino-tools'), + sprintf(__('پاک‌سازی انجام شد. %d رویداد قدیمی حذف شد.', 'sodino'), $deleted), + 'success' + ); + } + + wp_safe_redirect(admin_url('admin.php?page=sodino-tools')); + exit; } private function listUpsellsPage() { @@ -385,6 +424,9 @@ class AdminController { $upsell->target_product_id = max(0, intval($_POST['target_product_id'] ?? 0)); $upsell->discount_type = $discountType; $upsell->discount_value = max(0, floatval($_POST['discount_value'] ?? 0)); + if ($discountType === 'percentage') { + $upsell->discount_value = min(100, $upsell->discount_value); + } $upsell->priority = max(1, intval($_POST['priority'] ?? 10)); $upsell->status = isset($_POST['status']) ? 1 : 0; @@ -551,6 +593,80 @@ class AdminController { wp_send_json($results); } + private function getToolsData() { + global $wpdb; + + $tables = [ + 'rules' => [ + 'label' => __('قوانین قیمت‌گذاری', 'sodino'), + 'name' => $wpdb->prefix . 'sodino_rules', + ], + 'upsells' => [ + 'label' => __('آپسل‌ها', 'sodino'), + 'name' => $wpdb->prefix . 'sodino_upsells', + ], + 'banners' => [ + 'label' => __('بنرها', 'sodino'), + 'name' => $wpdb->prefix . 'sodino_banners', + ], + 'events' => [ + 'label' => __('رویدادهای تحلیلی', 'sodino'), + 'name' => $wpdb->prefix . 'sodino_events', + ], + 'analytics_cache' => [ + 'label' => __('کش تحلیلی', 'sodino'), + 'name' => $wpdb->prefix . 'sodino_analytics_cache', + ], + ]; + + foreach ($tables as $key => $table) { + $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $table['name'])); + $tables[$key]['exists'] = (bool) $exists; + $tables[$key]['count'] = $exists ? (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table['name']}") : 0; + } + + $eventsTable = $tables['events']['name']; + $oldEventCount = 0; + $oldestEvent = ''; + if ($tables['events']['exists']) { + $cutoff = date('Y-m-d H:i:s', current_time('timestamp') - (90 * DAY_IN_SECONDS)); + $oldEventCount = (int) $wpdb->get_var( + $wpdb->prepare("SELECT COUNT(*) FROM {$eventsTable} WHERE created_at < %s", $cutoff) + ); + $oldestEvent = (string) $wpdb->get_var("SELECT MIN(created_at) FROM {$eventsTable}"); + } + + return [ + 'db_version' => get_option('sodino_db_version', '0'), + 'expected_db_version' => defined('SODINO_DB_VERSION') ? SODINO_DB_VERSION : SODINO_VERSION, + 'settings' => $this->getSettings(), + 'tables' => $tables, + 'old_event_count' => $oldEventCount, + 'oldest_event' => $oldestEvent, + 'actions' => [ + 'clear_cache' => wp_nonce_url(admin_url('admin.php?page=sodino-tools&tool_action=clear_cache'), 'sodino_tools_clear_cache'), + 'run_migrations' => wp_nonce_url(admin_url('admin.php?page=sodino-tools&tool_action=run_migrations'), 'sodino_tools_run_migrations'), + 'prune_events' => wp_nonce_url(admin_url('admin.php?page=sodino-tools&tool_action=prune_events'), 'sodino_tools_prune_events'), + ], + ]; + } + + private function deleteOldEvents($days) { + global $wpdb; + + $days = max(1, (int) $days); + $eventsTable = $wpdb->prefix . 'sodino_events'; + $exists = $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $eventsTable)); + if (!$exists) { + return 0; + } + + $cutoff = date('Y-m-d H:i:s', current_time('timestamp') - ($days * DAY_IN_SECONDS)); + $deleted = $wpdb->query($wpdb->prepare("DELETE FROM {$eventsTable} WHERE created_at < %s", $cutoff)); + + return $deleted === false ? 0 : (int) $deleted; + } + private function getSettingsDefaults() { return [ 'plugin_enabled' => 1, diff --git a/app/Models/Upsell.php b/app/Models/Upsell.php index 7b82683..57bcf37 100644 --- a/app/Models/Upsell.php +++ b/app/Models/Upsell.php @@ -14,6 +14,8 @@ class Upsell { public $discount_value; public $status; public $priority; + public $impressions; + public $conversions; public $created_at; public $updated_at; @@ -27,6 +29,8 @@ class Upsell { $this->discount_value = isset($data['discount_value']) ? floatval($data['discount_value']) : 0; $this->status = isset($data['status']) ? (int) $data['status'] : 1; $this->priority = isset($data['priority']) ? (int) $data['priority'] : 10; + $this->impressions = isset($data['impressions']) ? (int) $data['impressions'] : 0; + $this->conversions = isset($data['conversions']) ? (int) $data['conversions'] : 0; $this->created_at = $data['created_at'] ?? null; $this->updated_at = $data['updated_at'] ?? null; } @@ -46,6 +50,8 @@ class Upsell { 'discount_value' => floatval($this->discount_value), 'status' => $this->status, 'priority' => $this->priority, + 'impressions' => $this->impressions, + 'conversions' => $this->conversions, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; diff --git a/app/Repositories/UpsellRepository.php b/app/Repositories/UpsellRepository.php index 7260ec1..a430b6f 100644 --- a/app/Repositories/UpsellRepository.php +++ b/app/Repositories/UpsellRepository.php @@ -66,4 +66,18 @@ class UpsellRepository { global $wpdb; return $wpdb->delete($this->table_name, ['id' => $id]); } + + public function incrementImpression($id) { + global $wpdb; + return $wpdb->query( + $wpdb->prepare("UPDATE {$this->table_name} SET impressions = impressions + 1 WHERE id = %d", $id) + ); + } + + public function incrementConversion($id) { + global $wpdb; + return $wpdb->query( + $wpdb->prepare("UPDATE {$this->table_name} SET conversions = conversions + 1 WHERE id = %d", $id) + ); + } } diff --git a/app/Services/UpsellService.php b/app/Services/UpsellService.php index 88ea39d..f5fc673 100644 --- a/app/Services/UpsellService.php +++ b/app/Services/UpsellService.php @@ -43,8 +43,18 @@ class UpsellService { } $price = floatval($product->get_price()); + return $this->calculateDiscountedPrice($price, $upsell); + } + + public function calculateDiscountedPrice($price, $upsell) { + $price = max(0, floatval($price)); + if (!$upsell) { + return $price; + } + if ($upsell->discount_type === 'percentage') { - return max(0, $price * (1 - floatval($upsell->discount_value) / 100)); + $percent = max(0, min(100, floatval($upsell->discount_value))); + return max(0, $price * (1 - $percent / 100)); } if ($upsell->discount_type === 'fixed') { @@ -54,6 +64,31 @@ class UpsellService { return $price; } + public function getById($id) { + return $this->upsellRepository->getById((int) $id); + } + + public function isValidForCart($upsell, $cart) { + return $cart && !$cart->is_empty() && $this->cartMatchesTrigger($upsell, $cart); + } + + public function canApplyToProduct($upsell, $productId, $variationId = 0) { + if (!$upsell || !$upsell->isActive()) { + return false; + } + + $targetProductId = (int) $upsell->target_product_id; + return $targetProductId > 0 && ($targetProductId === (int) $productId || $targetProductId === (int) $variationId); + } + + public function incrementImpression($upsellId) { + return $this->upsellRepository->incrementImpression((int) $upsellId); + } + + public function incrementConversion($upsellId) { + return $this->upsellRepository->incrementConversion((int) $upsellId); + } + public function getTriggerLabel($upsell) { switch ($upsell->trigger_type) { case 'product': diff --git a/database/migrations.php b/database/migrations.php index 188f8fb..253465e 100644 --- a/database/migrations.php +++ b/database/migrations.php @@ -214,5 +214,16 @@ function sodino_run_migrations($from_version) { if ($has_column($rules_table, 'actions')) { $wpdb->query("UPDATE {$rules_table} SET actions = '[]' WHERE actions IS NULL OR actions = ''"); } + + $upsell_table = $wpdb->prefix . 'sodino_upsells'; + if ($has_column($upsell_table, 'id')) { + if (!$has_column($upsell_table, 'impressions')) { + $wpdb->query("ALTER TABLE {$upsell_table} ADD COLUMN impressions bigint(20) NOT NULL DEFAULT 0 AFTER priority"); + } + + if (!$has_column($upsell_table, 'conversions')) { + $wpdb->query("ALTER TABLE {$upsell_table} ADD COLUMN conversions bigint(20) NOT NULL DEFAULT 0 AFTER impressions"); + } + } } } diff --git a/public/css/upsell-frontend.css b/public/css/upsell-frontend.css new file mode 100644 index 0000000..4ef3ab9 --- /dev/null +++ b/public/css/upsell-frontend.css @@ -0,0 +1,213 @@ +.sodino-upsell-panel { + direction: rtl; + margin: 0 0 28px; + padding: 22px; + color: #172033; + background: #ffffff; + border: 1px solid rgba(15, 23, 42, 0.12); + border-radius: 18px; + box-shadow: 0 18px 42px rgba(15, 23, 42, 0.08); +} + +.sodino-upsell-panel *, +.sodino-upsell-panel *::before, +.sodino-upsell-panel *::after { + box-sizing: border-box; +} + +.sodino-upsell-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 20px; +} + +.sodino-upsell-eyebrow { + margin: 0 0 6px; + color: #2563eb; + font-size: 13px; + font-weight: 700; +} + +.sodino-upsell-header h2 { + margin: 0; + color: #111827; + font-size: 22px; + line-height: 1.45; + font-weight: 800; +} + +.sodino-upsell-count { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + min-height: 32px; + padding: 6px 12px; + color: #1d4ed8; + background: #eff6ff; + border-radius: 999px; + font-size: 13px; + font-weight: 700; +} + +.sodino-upsell-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; +} + +.sodino-upsell-card { + display: grid; + grid-template-columns: 96px minmax(0, 1fr); + gap: 16px; + min-height: 168px; + padding: 16px; + background: #f8fafc; + border: 1px solid rgba(15, 23, 42, 0.1); + border-radius: 16px; +} + +.sodino-upsell-media { + width: 96px; + height: 96px; + overflow: hidden; + background: #ffffff; + border-radius: 14px; +} + +.sodino-upsell-image { + display: block; + width: 100%; + height: 100%; + object-fit: cover; +} + +.sodino-upsell-body { + min-width: 0; +} + +.sodino-upsell-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 10px; +} + +.sodino-upsell-meta span { + display: inline-flex; + align-items: center; + min-height: 26px; + padding: 4px 9px; + color: #047857; + background: #ecfdf5; + border-radius: 999px; + font-size: 12px; + font-weight: 700; +} + +.sodino-upsell-meta span + span { + color: #475569; + background: #e2e8f0; +} + +.sodino-upsell-title { + margin: 0 0 4px; + color: #475569; + font-size: 13px; + font-weight: 700; +} + +.sodino-upsell-card h3 { + margin: 0; + color: #111827; + font-size: 17px; + line-height: 1.45; + font-weight: 800; +} + +.sodino-upsell-footer { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 12px; + margin-top: 16px; +} + +.sodino-upsell-price { + display: grid; + gap: 4px; + color: #111827; + font-size: 17px; + font-weight: 800; +} + +.sodino-upsell-price del { + margin-right: 8px; + color: #94a3b8; + font-size: 13px; + font-weight: 600; +} + +.sodino-upsell-price small { + color: #047857; + font-size: 12px; + font-weight: 700; +} + +.sodino-upsell-button { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 42px; + padding: 10px 16px; + color: #ffffff !important; + background: #2563eb; + border-radius: 999px; + font-size: 13px; + font-weight: 800; + text-decoration: none !important; + transition: background-color 160ms ease, transform 160ms ease; +} + +.sodino-upsell-button:hover, +.sodino-upsell-button:focus { + color: #ffffff !important; + background: #1d4ed8; + transform: translateY(-1px); +} + +@media (max-width: 900px) { + .sodino-upsell-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 560px) { + .sodino-upsell-panel { + padding: 16px; + border-radius: 14px; + } + + .sodino-upsell-header, + .sodino-upsell-footer { + flex-direction: column; + align-items: stretch; + } + + .sodino-upsell-card { + grid-template-columns: 76px minmax(0, 1fr); + gap: 12px; + padding: 12px; + } + + .sodino-upsell-media { + width: 76px; + height: 76px; + } + + .sodino-upsell-button { + width: 100%; + } +} diff --git a/public/hooks/upsell-hooks.php b/public/hooks/upsell-hooks.php index 3766700..17d3cb3 100644 --- a/public/hooks/upsell-hooks.php +++ b/public/hooks/upsell-hooks.php @@ -12,6 +12,21 @@ $upsellRepository = new UpsellRepository(); $sodino_upsell_service = new UpsellService($upsellRepository); add_action('woocommerce_before_cart', 'sodino_render_upsell_suggestions'); +add_action('wp_enqueue_scripts', 'sodino_enqueue_upsell_assets'); +add_filter('woocommerce_add_cart_item_data', 'sodino_add_upsell_cart_item_data', 10, 4); +add_filter('woocommerce_get_cart_item_from_session', 'sodino_restore_upsell_cart_item_data', 10, 2); +add_action('woocommerce_before_calculate_totals', 'sodino_apply_upsell_cart_prices', 20); +add_filter('woocommerce_get_item_data', 'sodino_display_upsell_cart_item_data', 10, 2); +add_action('woocommerce_checkout_create_order_line_item', 'sodino_add_upsell_order_item_meta', 10, 4); +add_action('woocommerce_add_to_cart', 'sodino_track_upsell_conversion', 20, 6); + +function sodino_enqueue_upsell_assets() { + if (is_admin()) { + return; + } + + wp_enqueue_style('sodino-upsell-frontend', plugin_dir_url(__FILE__) . '../css/upsell-frontend.css', [], SODINO_VERSION); +} function sodino_render_upsell_suggestions() { if (is_admin() || !is_cart()) { @@ -41,19 +56,19 @@ function sodino_render_upsell_suggestions() { return; } - echo '
'; - echo '
'; - echo '
'; - echo '

' . esc_html__('پیشنهاد ویژه آپسل', 'sodino') . '

'; - echo '

' . esc_html__('این محصول را همراه خرید خود با تخفیف ویژه دریافت کنید', 'sodino') . '

'; + echo '
'; + echo '
'; + echo '
'; + echo '

' . esc_html__('پیشنهاد ویژه', 'sodino') . '

'; + echo '

' . esc_html__('این پیشنهادها را با تخفیف به سبد خود اضافه کنید', 'sodino') . '

'; echo '
'; - echo '' . count($upsells) . ' ' . esc_html__('پیشنهاد فعال', 'sodino') . ''; + echo '' . esc_html(sprintf(__('%d پیشنهاد فعال', 'sodino'), count($upsells))) . ''; echo '
'; - echo '
'; + echo '
'; foreach ($upsells as $upsell) { $product = wc_get_product($upsell->target_product_id); - if (!$product) { + if (!$product || !$product->is_purchasable() || !$product->is_in_stock() || !$product->is_type(['simple', 'variation'])) { continue; } @@ -61,31 +76,161 @@ function sodino_render_upsell_suggestions() { $originalPrice = floatval($product->get_price()); $priceHtml = wc_price($discountedPrice); if ($discountedPrice < $originalPrice) { - $priceHtml .= ' ' . wc_price($originalPrice) . ''; + $priceHtml .= ' ' . wc_price($originalPrice) . ''; } - $addToCartUrl = esc_url(add_query_arg('add-to-cart', $product->get_id(), wc_get_cart_url())); - $image = $product->get_image('woocommerce_thumbnail', ['class' => 'h-20 w-20 rounded-xl object-cover']); + $addToCartUrl = sodino_get_upsell_add_to_cart_url($product, $upsell); + $image = $product->get_image('woocommerce_thumbnail', ['class' => 'sodino-upsell-image']); + $savings = max(0, $originalPrice - $discountedPrice); + $sodino_upsell_service->incrementImpression($upsell->id); - echo ''; + echo '
'; +} + +function sodino_get_upsell_add_to_cart_url($product, $upsell) { + return add_query_arg( + [ + 'add-to-cart' => $product->get_id(), + 'quantity' => 1, + 'sodino_upsell_id' => $upsell->id, + 'sodino_upsell_nonce' => wp_create_nonce('sodino_apply_upsell_' . $upsell->id), + ], + wc_get_cart_url() + ); +} + +function sodino_add_upsell_cart_item_data($cart_item_data, $product_id, $variation_id, $quantity) { + if (empty($_REQUEST['sodino_upsell_id'])) { + return $cart_item_data; + } + + $upsellId = absint(wp_unslash($_REQUEST['sodino_upsell_id'])); + $nonce = sanitize_text_field(wp_unslash($_REQUEST['sodino_upsell_nonce'] ?? '')); + if (!$upsellId || !wp_verify_nonce($nonce, 'sodino_apply_upsell_' . $upsellId)) { + return $cart_item_data; + } + + global $sodino_upsell_service; + if (!isset($sodino_upsell_service) || !WC()->cart) { + return $cart_item_data; + } + + $upsell = $sodino_upsell_service->getById($upsellId); + if ( + !$sodino_upsell_service->canApplyToProduct($upsell, $product_id, $variation_id) + || !$sodino_upsell_service->isValidForCart($upsell, WC()->cart) + ) { + return $cart_item_data; + } + + $product = wc_get_product($variation_id ?: $product_id); + if (!$product) { + return $cart_item_data; + } + + $originalPrice = floatval($product->get_price()); + $discountedPrice = $sodino_upsell_service->calculateDiscountedPrice($originalPrice, $upsell); + + $cart_item_data['sodino_upsell'] = [ + 'id' => (int) $upsell->id, + 'title' => sanitize_text_field($upsell->title), + 'discount_type' => sanitize_key($upsell->discount_type), + 'discount_value' => floatval($upsell->discount_value), + 'original_price' => $originalPrice, + 'discounted_price' => $discountedPrice, + ]; + $cart_item_data['sodino_upsell_key'] = md5(wp_json_encode($cart_item_data['sodino_upsell']) . microtime(true)); + + return $cart_item_data; +} + +function sodino_restore_upsell_cart_item_data($cart_item, $values) { + if (!empty($values['sodino_upsell'])) { + $cart_item['sodino_upsell'] = $values['sodino_upsell']; + $cart_item['sodino_upsell_key'] = $values['sodino_upsell_key'] ?? ''; + } + return $cart_item; +} + +function sodino_apply_upsell_cart_prices($cart) { + if (is_admin() && !defined('DOING_AJAX')) { + return; + } + + if (!$cart) { + return; + } + + foreach ($cart->get_cart() as $cartItem) { + if (empty($cartItem['sodino_upsell']['discounted_price']) || empty($cartItem['data'])) { + continue; + } + + $cartItem['data']->set_price(max(0, floatval($cartItem['sodino_upsell']['discounted_price']))); + } +} + +function sodino_display_upsell_cart_item_data($item_data, $cart_item) { + if (empty($cart_item['sodino_upsell'])) { + return $item_data; + } + + $upsell = $cart_item['sodino_upsell']; + $item_data[] = [ + 'key' => __('پیشنهاد سودینو', 'sodino'), + 'value' => esc_html($upsell['title']), + ]; + + if (!empty($upsell['original_price']) && floatval($upsell['discounted_price']) < floatval($upsell['original_price'])) { + $item_data[] = [ + 'key' => __('تخفیف آپسل', 'sodino'), + 'value' => wp_kses_post(wc_price(floatval($upsell['original_price']) - floatval($upsell['discounted_price']))), + ]; + } + + return $item_data; +} + +function sodino_add_upsell_order_item_meta($item, $cart_item_key, $values, $order) { + if (empty($values['sodino_upsell'])) { + return; + } + + $upsell = $values['sodino_upsell']; + $item->add_meta_data('_sodino_upsell_id', (int) $upsell['id'], true); + $item->add_meta_data(__('پیشنهاد سودینو', 'sodino'), sanitize_text_field($upsell['title']), true); + $item->add_meta_data(__('قیمت اصلی آپسل', 'sodino'), wc_price(floatval($upsell['original_price'])), true); + $item->add_meta_data(__('قیمت تخفیفی آپسل', 'sodino'), wc_price(floatval($upsell['discounted_price'])), true); +} + +function sodino_track_upsell_conversion($cart_item_key, $product_id, $quantity, $variation_id, $variation, $cart_item_data) { + if (empty($cart_item_data['sodino_upsell']['id'])) { + return; + } + + global $sodino_upsell_service; + if (isset($sodino_upsell_service)) { + $sodino_upsell_service->incrementConversion((int) $cart_item_data['sodino_upsell']['id']); + } }