feat(upsell): apply real cart discounts and track performance
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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':
|
||||
|
||||
Reference in New Issue
Block a user