Files
sodino/app/Services/PricingService.php

306 lines
9.1 KiB
PHP

<?php
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 $settings;
private $cache;
private $trackedApplications = [];
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) {
if (!$this->settings->isPricingEnabled()) {
return $price;
}
if (!$price || !is_numeric($price)) {
return $price;
}
$price = $this->normalizePrice($price);
if (!$this->settings->get('cart_pricing_enabled') && is_cart()) {
return $price;
}
$originalPrice = $price;
$rules = $this->getApplicableRules($product);
if (empty($rules)) {
return $price;
}
if (!$this->settings->get('allow_multiple_rules')) {
$chosenRule = $this->chooseRule($rules, $price);
$rules = $chosenRule ? [$chosenRule] : [];
}
foreach ($rules as $rule) {
$oldPrice = $price;
$price = $this->applyRuleActions($rule, $price);
if ($price < $oldPrice) {
$this->trackDiscountOnce($product, $oldPrice, $price, $rule->id);
}
}
$price = $this->enforceLimits($originalPrice, $price);
return max(0, $price);
}
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);
});
return $rules[0] ?? null;
}
if ($strategy === 'first_valid') {
return $rules[0] ?? null;
}
usort($rules, function ($a, $b) {
return $b->priority <=> $a->priority;
});
return $rules[0] ?? null;
}
private function normalizePrice($price) {
if ($price === '' || $price === null) {
return 0.0;
}
return floatval($price);
}
private function ruleMatches($rule, $product = null) {
if (!$rule->isActive()) {
return false;
}
if ($rule->hasReachedLimit()) {
return false;
}
if (!empty($rule->user_roles) && is_array($rule->user_roles)) {
if (!$this->userHasAllowedRole($rule->user_roles)) {
return false;
}
}
if (empty($rule->conditions)) {
return true;
}
foreach ($rule->conditions as $condition) {
if (!$this->evaluateCondition($condition, $product)) {
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 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 userHasAllowedRole($roles) {
if (!is_user_logged_in()) {
return false;
}
$user = wp_get_current_user();
foreach ($roles as $role) {
if (in_array($role, $user->roles, true)) {
return true;
}
}
return false;
}
private function getCartTotal() {
if (!function_exists('WC') || !WC()->cart) {
return 0;
}
return floatval(WC()->cart->get_subtotal());
}
private function getCartItemCount() {
if (!function_exists('WC') || !WC()->cart) {
return 0;
}
return intval(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']);
if (is_wp_error($product_cats)) {
return false;
}
return (bool) array_intersect($product_cats, $this->normalizeIdList($categories));
}
private function productIsInIds($product, $ids) {
if (!$product || empty($ids)) {
return false;
}
return in_array($product->get_id(), $this->normalizeIdList($ids), true);
}
private function normalizeIdList($value) {
$values = is_array($value) ? $value : [$value];
$ids = [];
foreach ($values as $item) {
foreach (explode(',', (string) $item) as $id) {
$id = absint(trim($id));
if ($id > 0) {
$ids[] = $id;
}
}
}
return array_values(array_unique($ids));
}
private function applyRuleActions($rule, $price) {
foreach ($rule->actions as $action) {
$price = $this->applyAction($action, $price);
}
return $price;
}
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 'set_price':
return $value > 0 ? $value : $price;
case 'free_shipping':
return $price;
default:
return $price;
}
}
private function enforceLimits($originalPrice, $price) {
$minPrice = max(0, floatval($this->settings->get('min_product_price', 0)));
$price = max($price, $minPrice);
$maxDiscountPercent = floatval($this->settings->get('max_discount_percent', 100));
if ($maxDiscountPercent > 0 && $maxDiscountPercent < 100) {
$maxDiscount = $originalPrice * ($maxDiscountPercent / 100);
$minAllowedPrice = $originalPrice - $maxDiscount;
$price = max($minAllowedPrice, $price);
}
return $price;
}
private function estimateRuleDiscount($rule, $price) {
foreach ($rule->actions as $action) {
if (($action['type'] ?? '') === 'discount_percent') {
return $price * floatval($action['value']) / 100;
}
if (($action['type'] ?? '') === 'discount_fixed') {
return floatval($action['value']);
}
}
return 0;
}
private function trackDiscountOnce($product, $oldPrice, $price, $ruleId) {
$productId = $product ? $product->get_id() : 0;
$key = implode(':', [$productId, (int) $ruleId, round($oldPrice, 4), round($price, 4)]);
if (isset($this->trackedApplications[$key])) {
return;
}
$this->trackedApplications[$key] = true;
$this->trackingService->recordDiscountApplied($product, $oldPrice, $price, $ruleId);
$this->ruleRepository->incrementUsage($ruleId);
}
}