Init(Core): create and add project
This commit is contained in:
28
admin/admin.php
Normal file
28
admin/admin.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use Sodino\Controllers\AdminController;
|
||||
use Sodino\Repositories\RuleRepository;
|
||||
|
||||
// Initialize admin
|
||||
$ruleRepository = new RuleRepository();
|
||||
$adminController = new AdminController($ruleRepository);
|
||||
|
||||
// Add menu
|
||||
add_action('admin_menu', [$adminController, 'addMenu']);
|
||||
|
||||
// Enqueue admin assets
|
||||
add_action('admin_enqueue_scripts', function($hook) {
|
||||
if (strpos($hook, 'sodino') === false) {
|
||||
return;
|
||||
}
|
||||
wp_enqueue_style('sodino-admin', plugin_dir_url(__FILE__) . 'css/admin.css', [], SODINO_VERSION);
|
||||
});
|
||||
|
||||
// Handle delete for any Sodino admin page
|
||||
if (isset($_GET['page']) && strpos($_GET['page'], 'sodino') === 0 && isset($_GET['action']) && $_GET['action'] === 'delete') {
|
||||
add_action('admin_init', [$adminController, 'handleDelete']);
|
||||
}
|
||||
132
admin/class-rules-list-table.php
Normal file
132
admin/class-rules-list-table.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!class_exists('WP_List_Table')) {
|
||||
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||
}
|
||||
|
||||
class Sodino_Rules_List_Table extends WP_List_Table {
|
||||
private $repository;
|
||||
private $items_per_page = 20;
|
||||
|
||||
public function __construct($repository) {
|
||||
parent::__construct([
|
||||
'singular' => 'sodino_rule',
|
||||
'plural' => 'sodino_rules',
|
||||
'ajax' => false,
|
||||
]);
|
||||
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function get_columns() {
|
||||
return [
|
||||
'cb' => '<input type="checkbox" />',
|
||||
'name' => __('عنوان قانون', 'sodino'),
|
||||
'condition_type' => __('نوع کاربر', 'sodino'),
|
||||
'action_value' => __('درصد تخفیف', 'sodino'),
|
||||
'enabled' => __('وضعیت', 'sodino'),
|
||||
'actions' => __('عملیات', 'sodino'),
|
||||
];
|
||||
}
|
||||
|
||||
protected function get_sortable_columns() {
|
||||
return [
|
||||
'name' => ['name', true],
|
||||
];
|
||||
}
|
||||
|
||||
protected function column_cb($item) {
|
||||
return sprintf('<input type="checkbox" name="rule_ids[]" value="%d" />', $item->id);
|
||||
}
|
||||
|
||||
public function get_bulk_actions() {
|
||||
return [
|
||||
'delete' => __('حذف گروهی', 'sodino'),
|
||||
];
|
||||
}
|
||||
|
||||
public function column_actions($item) {
|
||||
$edit_url = admin_url('admin.php?page=sodino-add-rule&action=edit&id=' . $item->id);
|
||||
$delete_url = wp_nonce_url(admin_url('admin.php?page=sodino-rules&action=delete&id=' . $item->id), 'delete_rule');
|
||||
|
||||
return sprintf(
|
||||
'<a href="%s">%s</a> | <a href="%s" onclick="return confirm(\'%s\');">%s</a>',
|
||||
esc_url($edit_url),
|
||||
esc_html__('ویرایش', 'sodino'),
|
||||
esc_url($delete_url),
|
||||
esc_js(__('آیا از حذف این قانون مطمئن هستید؟', 'sodino')),
|
||||
esc_html__('حذف', 'sodino')
|
||||
);
|
||||
}
|
||||
|
||||
public function column_name($item) {
|
||||
$edit_url = admin_url('admin.php?page=sodino-add-rule&action=edit&id=' . $item->id);
|
||||
$title = sprintf('<strong><a href="%s">%s</a></strong>', esc_url($edit_url), esc_html($item->name));
|
||||
return $title;
|
||||
}
|
||||
|
||||
public function column_condition_type($item) {
|
||||
$value = __('کاربر جدید', 'sodino');
|
||||
if ($item->condition_value === 'returning') {
|
||||
$value = __('کاربر بازگشتی', 'sodino');
|
||||
}
|
||||
return esc_html($value);
|
||||
}
|
||||
|
||||
public function column_action_value($item) {
|
||||
return sprintf('%s %%', esc_html($item->action_value));
|
||||
}
|
||||
|
||||
public function column_enabled($item) {
|
||||
return $item->enabled ? __('فعال', 'sodino') : __('غیرفعال', 'sodino');
|
||||
}
|
||||
|
||||
public function column_default($item, $column_name) {
|
||||
switch ($column_name) {
|
||||
case 'name':
|
||||
case 'condition_type':
|
||||
case 'action_value':
|
||||
case 'enabled':
|
||||
case 'actions':
|
||||
return '';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
public function prepare_items() {
|
||||
$columns = $this->get_columns();
|
||||
$hidden = [];
|
||||
$sortable = $this->get_sortable_columns();
|
||||
|
||||
$this->_column_headers = [$columns, $hidden, $sortable];
|
||||
|
||||
$this->process_bulk_action();
|
||||
|
||||
$all_items = $this->repository->getAll();
|
||||
$current_page = $this->get_pagenum();
|
||||
$total_items = count($all_items);
|
||||
|
||||
$this->items = array_slice($all_items, ($current_page - 1) * $this->items_per_page, $this->items_per_page);
|
||||
|
||||
$this->set_pagination_args([
|
||||
'total_items' => $total_items,
|
||||
'per_page' => $this->items_per_page,
|
||||
'total_pages' => ceil($total_items / $this->items_per_page),
|
||||
]);
|
||||
}
|
||||
|
||||
public function process_bulk_action() {
|
||||
if ('delete' === $this->current_action()) {
|
||||
$rule_ids = isset($_POST['rule_ids']) ? array_map('intval', $_POST['rule_ids']) : [];
|
||||
if (!empty($rule_ids) && check_admin_referer('bulk-' . $this->_args['plural'])) {
|
||||
foreach ($rule_ids as $id) {
|
||||
$this->repository->delete($id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
admin/css/admin.css
Normal file
22
admin/css/admin.css
Normal file
@@ -0,0 +1,22 @@
|
||||
.wrap {
|
||||
direction: rtl;
|
||||
}
|
||||
|
||||
.sodino-admin-table th,
|
||||
.sodino-admin-table td {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.page-title-action {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.rtl .page-title-action {
|
||||
float: left;
|
||||
}
|
||||
|
||||
input.regular-text,
|
||||
select.regular-text,
|
||||
input.small-text {
|
||||
direction: rtl;
|
||||
}
|
||||
54
admin/views/rule-form.php
Normal file
54
admin/views/rule-form.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php echo $rule->id ? __('ویرایش قانون', 'sodino') : __('افزودن قانون جدید', 'sodino'); ?></h1>
|
||||
|
||||
<form method="post">
|
||||
<?php wp_nonce_field('gheymatyar_save_rule', 'gheymatyar_rule_nonce'); ?>
|
||||
<table class="form-table">
|
||||
<tr>
|
||||
<th scope="row"><label for="name"><?php _e('عنوان قانون', 'sodino'); ?></label></th>
|
||||
<td><input type="text" name="name" id="name" value="<?php echo esc_attr($rule->name); ?>" class="regular-text" required></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="condition_type"><?php _e('نوع شرط', 'sodino'); ?></label></th>
|
||||
<td>
|
||||
<select name="condition_type" id="condition_type" class="regular-text" required>
|
||||
<option value="user_type" <?php selected($rule->condition_type, 'user_type'); ?>><?php _e('نوع کاربر', 'sodino'); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="condition_value"><?php _e('نوع کاربر', 'sodino'); ?></label></th>
|
||||
<td>
|
||||
<select name="condition_value" id="condition_value" class="regular-text" required>
|
||||
<option value="new" <?php selected($rule->condition_value, 'new'); ?>><?php _e('کاربر جدید', 'sodino'); ?></option>
|
||||
<option value="returning" <?php selected($rule->condition_value, 'returning'); ?>><?php _e('کاربر بازگشتی', 'sodino'); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="action_type"><?php _e('نوع عملیات', 'sodino'); ?></label></th>
|
||||
<td>
|
||||
<select name="action_type" id="action_type" class="regular-text" required>
|
||||
<option value="discount_percent" <?php selected($rule->action_type, 'discount_percent'); ?>><?php _e('درصد تخفیف', 'sodino'); ?></option>
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="action_value"><?php _e('درصد تخفیف', 'sodino'); ?></label></th>
|
||||
<td><input type="number" name="action_value" id="action_value" value="<?php echo esc_attr($rule->action_value); ?>" min="0" max="100" step="0.01" class="small-text" required> %</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><label for="enabled"><?php _e('وضعیت', 'sodino'); ?></label></th>
|
||||
<td><label><input type="checkbox" name="enabled" id="enabled" value="1" <?php checked($rule->enabled, 1); ?>> <?php _e('فعال باشد', 'sodino'); ?></label></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<?php submit_button($rule->id ? __('بهروزرسانی قانون', 'sodino') : __('افزودن قانون', 'sودino'), 'primary'); ?>
|
||||
</form>
|
||||
</div>
|
||||
15
admin/views/rules-list.php
Normal file
15
admin/views/rules-list.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php _e('قوانین قیمتگذاری', 'sodino'); ?></h1>
|
||||
|
||||
<a href="<?php echo admin_url('admin.php?page=sodino-add-rule'); ?>" class="page-title-action"><?php _e('افزودن قانون جدید', 'sodino'); ?></a>
|
||||
|
||||
<form method="post">
|
||||
<?php $rulesTable->display(); ?>
|
||||
</form>
|
||||
</div>
|
||||
12
admin/views/settings.php
Normal file
12
admin/views/settings.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php _e('تنظیمات قیمتیار', 'sodino'); ?></h1>
|
||||
<div class="notice notice-info">
|
||||
<p><?php _e('در اینجا تنظیمات افزونه در آینده قرار خواهد گرفت.', 'sodino'); ?></p>
|
||||
</div>
|
||||
</div>
|
||||
158
app/Controllers/AdminController.php
Normal file
158
app/Controllers/AdminController.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
namespace Sodino\Controllers;
|
||||
|
||||
use Sodino\Repositories\RuleRepository;
|
||||
use Sodino\Models\Rule;
|
||||
|
||||
/**
|
||||
* Admin Controller
|
||||
*/
|
||||
class AdminController {
|
||||
private $ruleRepository;
|
||||
|
||||
public function __construct(RuleRepository $ruleRepository) {
|
||||
$this->ruleRepository = $ruleRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle admin menu
|
||||
*/
|
||||
public function addMenu() {
|
||||
add_menu_page(
|
||||
__('قیمتیار', 'sodino'),
|
||||
__('قیمتیار', 'sodino'),
|
||||
'manage_options',
|
||||
'sodino-rules',
|
||||
[$this, 'rulesPage'],
|
||||
'dashicons-money-alt',
|
||||
56
|
||||
);
|
||||
|
||||
add_submenu_page(
|
||||
'sodino-rules',
|
||||
__('قوانین قیمتگذاری', 'sodino'),
|
||||
__('قوانین قیمتگذاری', 'sودino'),
|
||||
'manage_options',
|
||||
'sodino-rules',
|
||||
[$this, 'rulesPage']
|
||||
);
|
||||
|
||||
add_submenu_page(
|
||||
'sodino-rules',
|
||||
__('افزودن قانون', 'sودino'),
|
||||
__('افزودن قانون', 'sودino'),
|
||||
'manage_options',
|
||||
'sodino-add-rule',
|
||||
[$this, 'addRulePage']
|
||||
);
|
||||
|
||||
add_submenu_page(
|
||||
'sodino-rules',
|
||||
__('تنظیمات', 'sodino'),
|
||||
__('تنظیمات', 'sودینو'),
|
||||
'manage_options',
|
||||
'sodino-settings',
|
||||
[$this, 'settingsPage']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rules admin page
|
||||
*/
|
||||
public function rulesPage() {
|
||||
$this->listRulesPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* List rules page
|
||||
*/
|
||||
private function listRulesPage() {
|
||||
require_once SODINO_PLUGIN_DIR . 'admin/class-rules-list-table.php';
|
||||
|
||||
$rulesTable = new \Sodino_Rules_List_Table($this->ruleRepository);
|
||||
$rulesTable->prepare_items();
|
||||
|
||||
include SODINO_PLUGIN_DIR . 'admin/views/rules-list.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add or edit rule page
|
||||
*/
|
||||
public function addRulePage() {
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'edit') {
|
||||
return $this->editRulePage();
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->saveRule();
|
||||
} else {
|
||||
$rule = new Rule();
|
||||
include SODINO_PLUGIN_DIR . 'admin/views/rule-form.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings page
|
||||
*/
|
||||
public function settingsPage() {
|
||||
include SODINO_PLUGIN_DIR . 'admin/views/settings.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit rule page
|
||||
*/
|
||||
private function editRulePage() {
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
$rule = $this->ruleRepository->getById($id);
|
||||
|
||||
if (!$rule) {
|
||||
wp_die(__('Rule not found', 'sodino'));
|
||||
}
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$this->saveRule($rule);
|
||||
} else {
|
||||
include SODINO_PLUGIN_DIR . 'admin/views/rule-form.php';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save rule
|
||||
*/
|
||||
private function saveRule($rule = null) {
|
||||
if (!isset($_POST['gheymatyar_rule_nonce']) || !wp_verify_nonce($_POST['gheymatyar_rule_nonce'], 'gheymatyar_save_rule')) {
|
||||
wp_die(__('خطای امنیتی رخ داد.', 'sodino'));
|
||||
}
|
||||
|
||||
if (!$rule) {
|
||||
$rule = new Rule();
|
||||
}
|
||||
|
||||
$rule->name = sanitize_text_field($_POST['name'] ?? '');
|
||||
$rule->condition_type = sanitize_text_field($_POST['condition_type'] ?? 'user_type');
|
||||
$rule->condition_value = sanitize_text_field($_POST['condition_value'] ?? 'new');
|
||||
$rule->action_type = sanitize_text_field($_POST['action_type'] ?? 'discount_percent');
|
||||
$rule->action_value = sanitize_text_field($_POST['action_value'] ?? '0');
|
||||
$rule->enabled = isset($_POST['enabled']) ? 1 : 0;
|
||||
|
||||
$this->ruleRepository->save($rule);
|
||||
|
||||
wp_safe_redirect(admin_url('admin.php?page=sodino-rules'));
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle delete action
|
||||
*/
|
||||
public function handleDelete() {
|
||||
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);
|
||||
|
||||
wp_redirect(admin_url('admin.php?page=sodino-rules'));
|
||||
exit;
|
||||
}
|
||||
}
|
||||
85
app/Models/Rule.php
Normal file
85
app/Models/Rule.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
namespace Sodino\Models;
|
||||
|
||||
/**
|
||||
* Rule Model
|
||||
*/
|
||||
class Rule {
|
||||
public $id;
|
||||
public $name;
|
||||
public $conditions;
|
||||
public $actions;
|
||||
public $priority;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct($data = []) {
|
||||
$this->id = $data['id'] ?? null;
|
||||
$this->name = $data['name'] ?? '';
|
||||
$this->conditions = $this->parseJsonField($data['conditions'] ?? '[]');
|
||||
$this->actions = $this->parseJsonField($data['actions'] ?? '[]');
|
||||
$this->priority = isset($data['priority']) ? (int) $data['priority'] : 10;
|
||||
$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) {
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$decoded = json_decode($value, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert to array
|
||||
*/
|
||||
public function toArray() {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->name,
|
||||
'conditions' => wp_json_encode($this->conditions),
|
||||
'actions' => wp_json_encode($this->actions),
|
||||
'priority' => $this->priority,
|
||||
'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,
|
||||
];
|
||||
}
|
||||
}
|
||||
76
app/Repositories/RuleRepository.php
Normal file
76
app/Repositories/RuleRepository.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
namespace Sodino\Repositories;
|
||||
|
||||
use Sodino\Models\Rule;
|
||||
|
||||
/**
|
||||
* Rule Repository
|
||||
*/
|
||||
class RuleRepository {
|
||||
private $table_name;
|
||||
|
||||
public function __construct() {
|
||||
global $wpdb;
|
||||
$this->table_name = $wpdb->prefix . 'sodino_rules';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save rule
|
||||
*/
|
||||
public function save(Rule $rule) {
|
||||
global $wpdb;
|
||||
$data = $rule->toArray();
|
||||
unset($data['id'], $data['created_at'], $data['updated_at']);
|
||||
|
||||
if ($rule->id) {
|
||||
$wpdb->update($this->table_name, $data, ['id' => $rule->id]);
|
||||
return $rule->id;
|
||||
} else {
|
||||
$wpdb->insert($this->table_name, $data);
|
||||
return $wpdb->insert_id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete rule
|
||||
*/
|
||||
public function delete($id) {
|
||||
global $wpdb;
|
||||
return $wpdb->delete($this->table_name, ['id' => $id]);
|
||||
}
|
||||
}
|
||||
210
app/Services/PricingService.php
Normal file
210
app/Services/PricingService.php
Normal file
@@ -0,0 +1,210 @@
|
||||
<?php
|
||||
namespace Sodino\Services;
|
||||
|
||||
use Sodino\Repositories\RuleRepository;
|
||||
|
||||
/**
|
||||
* Pricing Service
|
||||
*/
|
||||
class PricingService {
|
||||
private $ruleRepository;
|
||||
private $rulesCache = null;
|
||||
private $freeShipping = false;
|
||||
|
||||
public function __construct(RuleRepository $ruleRepository) {
|
||||
$this->ruleRepository = $ruleRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply dynamic pricing to a product price
|
||||
*/
|
||||
public function applyDynamicPricing($price, $product) {
|
||||
$price = $this->normalizePrice($price);
|
||||
$rules = $this->getEnabledRules();
|
||||
$matchedRule = null;
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
if ($this->ruleMatches($rule, $product)) {
|
||||
if ($matchedRule === null || $rule->priority > $matchedRule->priority) {
|
||||
$matchedRule = $rule;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchedRule) {
|
||||
$price = $this->applyActions($matchedRule, $price);
|
||||
}
|
||||
|
||||
return max(0, $price);
|
||||
}
|
||||
|
||||
public function shouldApplyFreeShipping() {
|
||||
$rules = $this->getEnabledRules();
|
||||
foreach ($rules as $rule) {
|
||||
if ($this->ruleMatches($rule, null) && $this->ruleHasFreeShipping($rule)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function resetFreeShippingFlag() {
|
||||
$this->freeShipping = false;
|
||||
}
|
||||
|
||||
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 getUserType() {
|
||||
if (!is_user_logged_in()) {
|
||||
return 'guest';
|
||||
}
|
||||
|
||||
$user_id = get_current_user_id();
|
||||
$order_count = wc_get_customer_order_count($user_id);
|
||||
|
||||
return $order_count > 0 ? 'returning' : 'new';
|
||||
}
|
||||
|
||||
private function ruleMatches($rule, $product = null) {
|
||||
if (!$this->isRuleActive($rule)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($rule->conditions as $condition) {
|
||||
if (!$this->evaluateCondition($condition, $product)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function isRuleActive($rule) {
|
||||
if (!$rule->enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$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;
|
||||
|
||||
switch ($type) {
|
||||
case 'user_type':
|
||||
return $this->getUserType() === $value;
|
||||
case 'cart_total_min':
|
||||
return $this->getCartTotal() >= floatval($value);
|
||||
case 'cart_total_max':
|
||||
return $this->getCartTotal() <= floatval($value);
|
||||
case 'cart_item_count_min':
|
||||
return $this->getCartItemCount() >= intval($value);
|
||||
case 'cart_item_count_max':
|
||||
return $this->getCartItemCount() <= intval($value);
|
||||
case 'product_category':
|
||||
return $this->productHasCategory($product, (array) $value);
|
||||
case 'product_ids':
|
||||
return $this->productIsInIds($product, (array) $value);
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private function getCartTotal() {
|
||||
if (!WC()->cart) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return floatval(WC()->cart->get_cart_contents_total());
|
||||
}
|
||||
|
||||
private function getCartItemCount() {
|
||||
if (!WC()->cart) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return WC()->cart->get_cart_contents_count();
|
||||
}
|
||||
|
||||
private function productHasCategory($product, $categories) {
|
||||
if (!$product || empty($categories)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$product_cats = wp_get_post_terms($product->get_id(), 'product_cat', ['fields' => 'ids']);
|
||||
return (bool) array_intersect($product_cats, $categories);
|
||||
}
|
||||
|
||||
private function productIsInIds($product, $ids) {
|
||||
if (!$product || empty($ids)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array($product->get_id(), $ids, true);
|
||||
}
|
||||
|
||||
private function applyActions($rule, $price) {
|
||||
foreach ($rule->actions as $action) {
|
||||
$price = $this->applyAction($action, $price);
|
||||
if (($action['type'] ?? '') === 'free_shipping') {
|
||||
$this->freeShipping = true;
|
||||
}
|
||||
}
|
||||
return $price;
|
||||
}
|
||||
|
||||
private function ruleHasFreeShipping($rule) {
|
||||
foreach ($rule->actions as $action) {
|
||||
if (($action['type'] ?? '') === 'free_shipping') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function applyAction($action, $price) {
|
||||
$type = $action['type'] ?? '';
|
||||
$value = isset($action['value']) ? floatval($action['value']) : 0;
|
||||
|
||||
switch ($type) {
|
||||
case 'discount_percent':
|
||||
if ($value <= 0) {
|
||||
return $price;
|
||||
}
|
||||
return $price * (1 - $value / 100);
|
||||
case 'discount_fixed':
|
||||
if ($value <= 0) {
|
||||
return $price;
|
||||
}
|
||||
return $price - $value;
|
||||
case 'free_shipping':
|
||||
return $price;
|
||||
default:
|
||||
return $price;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
database/migrations.php
Normal file
58
database/migrations.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database migrations for Sodino plugin
|
||||
*/
|
||||
|
||||
function sodino_create_tables() {
|
||||
global $wpdb;
|
||||
|
||||
$charset_collate = $wpdb->get_charset_collate();
|
||||
|
||||
// Rules table
|
||||
$rules_table = $wpdb->prefix . 'sodino_rules';
|
||||
$rules_sql = "CREATE TABLE $rules_table (
|
||||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||||
name varchar(255) NOT NULL,
|
||||
conditions longtext NOT NULL,
|
||||
actions longtext NOT NULL,
|
||||
priority int(11) NOT NULL DEFAULT 10,
|
||||
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)
|
||||
) $charset_collate;";
|
||||
|
||||
// Upsell table
|
||||
$upsell_table = $wpdb->prefix . 'sodino_upsells';
|
||||
$upsell_sql = "CREATE TABLE $upsell_table (
|
||||
id mediumint(9) NOT NULL AUTO_INCREMENT,
|
||||
name varchar(255) NOT NULL,
|
||||
triggers longtext NOT NULL,
|
||||
suggestions longtext NOT NULL,
|
||||
discount_type varchar(50) DEFAULT 'percentage',
|
||||
discount_value varchar(50) DEFAULT '0',
|
||||
enabled tinyint(1) DEFAULT 1,
|
||||
priority int(11) NOT NULL DEFAULT 10,
|
||||
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
) $charset_collate;";
|
||||
|
||||
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||
dbDelta($rules_sql);
|
||||
dbDelta($upsell_sql);
|
||||
|
||||
// Add version option
|
||||
add_option('sodino_db_version', '1.1');
|
||||
}
|
||||
34
public/hooks/pricing-hooks.php
Normal file
34
public/hooks/pricing-hooks.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
use Sodino\Services\PricingService;
|
||||
use Sodino\Repositories\RuleRepository;
|
||||
|
||||
// Initialize pricing service
|
||||
global $sodino_pricing_service;
|
||||
$ruleRepository = new RuleRepository();
|
||||
$sodino_pricing_service = new PricingService($ruleRepository);
|
||||
|
||||
// Hook into WooCommerce price filter
|
||||
add_filter('woocommerce_product_get_price', 'sodino_apply_dynamic_pricing', 10, 2);
|
||||
add_filter('woocommerce_product_get_sale_price', 'sodino_apply_dynamic_pricing', 10, 2);
|
||||
add_filter('woocommerce_product_variation_get_price', 'sodino_apply_dynamic_pricing', 10, 2);
|
||||
add_filter('woocommerce_product_variation_get_sale_price', 'sodino_apply_dynamic_pricing', 10, 2);
|
||||
|
||||
// Also hook into cart and checkout prices
|
||||
add_filter('woocommerce_cart_item_price', 'sodino_apply_to_cart_item', 10, 3);
|
||||
add_filter('woocommerce_cart_item_subtotal', 'sodino_apply_to_cart_item', 10, 3);
|
||||
|
||||
function sodino_apply_dynamic_pricing($price, $product) {
|
||||
global $sodino_pricing_service;
|
||||
return $sodino_pricing_service->applyDynamicPricing($price, $product);
|
||||
}
|
||||
|
||||
function sodino_apply_to_cart_item($price, $cart_item, $cart_item_key) {
|
||||
global $sodino_pricing_service;
|
||||
$product = $cart_item['data'];
|
||||
return wc_price($sodino_pricing_service->applyDynamicPricing($product->get_price(), $product));
|
||||
}
|
||||
38
readme.txt
Normal file
38
readme.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
=== Sodino ===
|
||||
Contributors: yourname
|
||||
Tags: woocommerce, pricing, dynamic pricing, revenue optimization
|
||||
Requires at least: 5.0
|
||||
Tested up to: 6.0
|
||||
Requires PHP: 7.4
|
||||
Stable tag: 1.0.0
|
||||
License: GPLv2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
Smart Pricing & Revenue Optimization plugin for WooCommerce.
|
||||
|
||||
== Description ==
|
||||
|
||||
Sodino dynamically adjusts WooCommerce product prices based on user behavior and predefined rules.
|
||||
|
||||
Features:
|
||||
- Dynamic pricing based on user type (new vs returning)
|
||||
- Rule-based system for discounts
|
||||
- Admin panel to manage rules
|
||||
- MVC architecture for extensibility
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Upload the plugin files to the `/wp-content/plugins/sodino` directory, or install the plugin through the WordPress plugins screen directly.
|
||||
2. Activate the plugin through the 'Plugins' screen in WordPress.
|
||||
3. Use the Sodino menu in the admin to configure rules.
|
||||
|
||||
== Frequently Asked Questions ==
|
||||
|
||||
= Does it work with variable products? =
|
||||
|
||||
Yes, it applies to all product types.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 1.0.0 =
|
||||
* Initial release.
|
||||
100
sodino.php
Normal file
100
sodino.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Sodino (سودینو)
|
||||
* Plugin URI: https://example.com/sodino
|
||||
* Description: افزونه هوشمند قیمتگذاری و بهینهسازی درآمد برای ووکامرس. قیمت محصولات را بر اساس رفتار کاربر و قوانین تعریفشده به صورت پویا تنظیم میکند.
|
||||
* Version: 1.0.0
|
||||
* Author: Your Name
|
||||
* License: GPL v2 or later
|
||||
* Text Domain: sodino
|
||||
* Requires at least: 5.0
|
||||
* Tested up to: 6.0
|
||||
* Requires PHP: 7.4
|
||||
* WC requires at least: 5.0
|
||||
* WC tested up to: 6.0
|
||||
*/
|
||||
|
||||
// Prevent direct access
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Define plugin constants
|
||||
define('SODINO_VERSION', '1.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__));
|
||||
|
||||
// Autoloader for PSR-4
|
||||
spl_autoload_register(function ($class) {
|
||||
// Namespace prefix
|
||||
$prefix = 'Sodino\\';
|
||||
|
||||
// Base directory for the namespace prefix
|
||||
$base_dir = SODINO_PLUGIN_DIR . 'app/';
|
||||
|
||||
// Does the class use the namespace prefix?
|
||||
$len = strlen($prefix);
|
||||
if (strncmp($prefix, $class, $len) !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the relative class name
|
||||
$relative_class = substr($class, $len);
|
||||
|
||||
// Replace namespace separators with directory separators
|
||||
$file = $base_dir . str_replace('\\', '/', $relative_class) . '.php';
|
||||
|
||||
// If the file exists, require it
|
||||
if (file_exists($file)) {
|
||||
require $file;
|
||||
}
|
||||
});
|
||||
|
||||
// Activation hook
|
||||
register_activation_hook(__FILE__, 'sodino_activate');
|
||||
function sodino_activate() {
|
||||
// Include database migration
|
||||
require_once SODINO_PLUGIN_DIR . 'database/migrations.php';
|
||||
sodino_create_tables();
|
||||
|
||||
// Flush rewrite rules if needed
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
// Deactivation hook
|
||||
register_deactivation_hook(__FILE__, 'sodino_deactivate');
|
||||
function sodino_deactivate() {
|
||||
// Flush rewrite rules
|
||||
flush_rewrite_rules();
|
||||
}
|
||||
|
||||
// Bootstrap the plugin
|
||||
function sodino_init() {
|
||||
// Check if WooCommerce is active
|
||||
if (!class_exists('WooCommerce')) {
|
||||
add_action('admin_notices', 'sodino_woocommerce_missing_notice');
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize admin
|
||||
if (is_admin()) {
|
||||
require_once SODINO_PLUGIN_DIR . 'admin/admin.php';
|
||||
}
|
||||
|
||||
// Initialize public hooks
|
||||
require_once SODINO_PLUGIN_DIR . 'public/hooks/pricing-hooks.php';
|
||||
|
||||
// Load text domain
|
||||
load_plugin_textdomain('sodino', false, dirname(SODINO_PLUGIN_BASENAME) . '/languages/');
|
||||
}
|
||||
add_action('plugins_loaded', 'sodino_init');
|
||||
|
||||
// WooCommerce missing notice
|
||||
function sodino_woocommerce_missing_notice() {
|
||||
?>
|
||||
<div class="error">
|
||||
<p><?php _e('Sodino requires WooCommerce to be installed and active.', 'sodino'); ?></p>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
Reference in New Issue
Block a user