feat: Implement upsell functionality with repository and service layers

This commit is contained in:
2026-05-02 23:30:23 +03:30
parent 4928901a08
commit 5930c1ad6f
26 changed files with 3130 additions and 126 deletions

View File

@@ -2,54 +2,98 @@
namespace Sodino\Services;
use Sodino\Repositories\RuleRepository;
use Sodino\Services\TrackingService;
/**
* Pricing Service
*/
class PricingService {
private $ruleRepository;
private $trackingService;
private $rulesCache = null;
private $freeShipping = false;
public function __construct(RuleRepository $ruleRepository) {
public function __construct(RuleRepository $ruleRepository, TrackingService $trackingService) {
$this->ruleRepository = $ruleRepository;
$this->trackingService = $trackingService;
}
/**
* Apply dynamic pricing to a product price
*/
public function applyDynamicPricing($price, $product) {
$settings = $this->getSettings();
if (empty($settings['plugin_enabled']) || empty($settings['pricing_enabled'])) {
return $price;
}
if (!$price || !is_numeric($price)) {
return $price;
}
$price = $this->normalizePrice($price);
if (!$settings['cart_pricing_enabled'] && is_cart()) {
return $price;
}
$originalPrice = $price;
$rules = $this->getEnabledRules();
$matchedRule = null;
$matchedRules = [];
foreach ($rules as $rule) {
if ($this->ruleMatches($rule, $product)) {
if ($matchedRule === null || $rule->priority > $matchedRule->priority) {
$matchedRule = $rule;
}
$matchedRules[] = $rule;
}
}
if ($matchedRule) {
$price = $this->applyActions($matchedRule, $price);
if (empty($matchedRules)) {
return $price;
}
if (!$settings['allow_multiple_rules']) {
$chosenRule = $this->chooseRule($matchedRules, $price, $settings['strategy']);
$matchedRules = $chosenRule ? [$chosenRule] : [];
}
foreach ($matchedRules as $rule) {
$oldPrice = $price;
$price = $this->applyActions($rule, $price);
if ($price < $oldPrice) {
$this->trackingService->recordDiscountApplied($product, $oldPrice, $price, $rule->id);
}
}
$price = $this->enforceLimits($originalPrice, $price, $settings);
return max(0, $price);
}
public function shouldApplyFreeShipping() {
$rules = $this->getEnabledRules();
foreach ($rules as $rule) {
if ($this->ruleMatches($rule, null) && $this->ruleHasFreeShipping($rule)) {
return true;
}
private function chooseRule(array $rules, $price, $strategy) {
if ($strategy === 'highest_discount') {
usort($rules, function ($a, $b) use ($price) {
return $this->estimateRuleDiscount($b, $price) <=> $this->estimateRuleDiscount($a, $price);
});
return $rules[0] ?? null;
}
return false;
if ($strategy === 'first_valid') {
return $rules[0] ?? null;
}
usort($rules, function ($a, $b) {
return $b->priority <=> $a->priority;
});
return $rules[0] ?? null;
}
public function resetFreeShippingFlag() {
$this->freeShipping = false;
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() {
@@ -67,22 +111,29 @@ class PricingService {
return floatval($price);
}
private function getUserType() {
if (!is_user_logged_in()) {
return 'guest';
private function ruleMatches($rule, $product = null) {
if (!$rule->enabled) {
return false;
}
$user_id = get_current_user_id();
$order_count = wc_get_customer_order_count($user_id);
if ($rule->usage_limit > 0 && $this->trackingService->getRuleUsageCount($rule->id) >= $rule->usage_limit) {
return false;
}
return $order_count > 0 ? 'returning' : 'new';
}
if (!empty($rule->user_roles) && is_array($rule->user_roles)) {
if (!$this->userHasAllowedRole($rule->user_roles)) {
return false;
}
}
private function ruleMatches($rule, $product = null) {
if (!$this->isRuleActive($rule)) {
return false;
}
if (empty($rule->conditions)) {
return true;
}
foreach ($rule->conditions as $condition) {
if (!$this->evaluateCondition($condition, $product)) {
return false;
@@ -93,10 +144,6 @@ class PricingService {
}
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) {
@@ -134,6 +181,32 @@ class PricingService {
}
}
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 (!WC()->cart) {
return 0;
@@ -170,20 +243,9 @@ class PricingService {
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;
return $price;
}
private function applyAction($action, $price) {
@@ -207,4 +269,29 @@ class PricingService {
return $price;
}
}
private function enforceLimits($originalPrice, $price, array $settings) {
$minPrice = max(0, floatval($settings['min_product_price']));
$price = max($price, $minPrice);
$maxDiscountPercent = floatval($settings['max_discount_percent']);
if ($maxDiscountPercent > 0 && $maxDiscountPercent < 100) {
$limit = $originalPrice * ($maxDiscountPercent / 100);
$price = max($originalPrice - $limit, $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;
}
}