diff --git a/core/components/minishop3/src/Controllers/Api/Manager/OrdersController.php b/core/components/minishop3/src/Controllers/Api/Manager/OrdersController.php index b91ace53..e31b712c 100644 --- a/core/components/minishop3/src/Controllers/Api/Manager/OrdersController.php +++ b/core/components/minishop3/src/Controllers/Api/Manager/OrdersController.php @@ -348,7 +348,7 @@ public function recalculateCost(array $params = []): array $options['manual_delivery_cost'] = $params['manual_delivery_cost']; } - $recalculator = new ManagerOrderCostRecalculator($this->modx, $ms3); + $recalculator = $this->modx->services->get('ms3_manager_order_cost_recalculator'); $result = $recalculator->recalculate($order, $options); if (empty($result['success'])) { diff --git a/core/components/minishop3/src/Controllers/Delivery/Delivery.php b/core/components/minishop3/src/Controllers/Delivery/Delivery.php index fbdb1114..8d05f397 100644 --- a/core/components/minishop3/src/Controllers/Delivery/Delivery.php +++ b/core/components/minishop3/src/Controllers/Delivery/Delivery.php @@ -5,7 +5,7 @@ use MiniShop3\MiniShop3; use MiniShop3\Model\msDelivery; use MiniShop3\Model\msOrder; -use MiniShop3\Utils\PriceAdjustment; +use MiniShop3\Services\Order\OrderCostEngine; use MODX\Revolution\modX; /** @@ -77,8 +77,6 @@ public function __construct(MiniShop3 $ms3, array $config = []) */ public function getCost(msOrder $order, msDelivery $delivery, float $cost): float { - $deliveryCost = 0; - // Get cart data for weight calculation $cart = [ 'total_weight' => 0, @@ -98,51 +96,14 @@ public function getCost(msOrder $order, msDelivery $delivery, float $cost): floa } } - // Check free delivery threshold first - // Use $cost parameter (cart cost passed from calculator) for threshold check - $freeDeliveryAmount = (float)$delivery->get('free_delivery_amount'); - - if ($freeDeliveryAmount > 0 && $cost >= $freeDeliveryAmount) { - return 0; - } - - // Cost by weight - $weightPrice = (float)$delivery->get('weight_price'); - $cartWeight = (float)($cart['total_weight'] ?? 0); - - if ($weightPrice < 0) { - $this->modx->log( - modX::LOG_LEVEL_ERROR, - "[Delivery] Invalid weight_price for delivery #{$delivery->get('id')}: {$weightPrice}. Must be >= 0." - ); - $weightPrice = 0; - } - - $deliveryCost += $weightPrice * $cartWeight; - - // Base delivery cost - $addPrice = $delivery->get('price'); - - if (empty($addPrice)) { - return $deliveryCost; - } - - if (PriceAdjustment::isPercent($addPrice)) { - $percent = PriceAdjustment::getPercent($addPrice); - if (!PriceAdjustment::isAllowedPercent($percent)) { - $this->modx->log( - modX::LOG_LEVEL_ERROR, - sprintf( - '[Delivery] Invalid percent value for delivery #%s: %s%%. Must be between -100%% and 100%%.', - $delivery->get('id'), - $percent - ) - ); - return $deliveryCost; - } - } + $cartWeight = (float) ($cart['total_weight'] ?? 0); - return $deliveryCost + PriceAdjustment::calculate($cost, $addPrice); + return OrderCostEngine::calculateDefaultDeliveryCost( + $this->modx, + $delivery, + $cost, + $cartWeight + ); } /** diff --git a/core/components/minishop3/src/Controllers/Payment/Payment.php b/core/components/minishop3/src/Controllers/Payment/Payment.php index d0ade161..9bbe4e8c 100644 --- a/core/components/minishop3/src/Controllers/Payment/Payment.php +++ b/core/components/minishop3/src/Controllers/Payment/Payment.php @@ -5,7 +5,7 @@ use MiniShop3\MiniShop3; use MiniShop3\Model\msOrder; use MiniShop3\Model\msPayment; -use MiniShop3\Utils\PriceAdjustment; +use MiniShop3\Services\Order\OrderCostEngine; use MODX\Revolution\modX; /** @@ -137,33 +137,14 @@ abstract public function receive(msOrder $order): array; * * @param msOrder $order Order (can be used for fee calculation) * @param msPayment $payment Payment method with fee settings - * @param float $cost Current order cost + * @param float $cost Commission base before surcharge: cart lines + delivery (#372), not cart alone * @return float Cost including fee */ public function getCost(msOrder $order, msPayment $payment, float $cost): float { - $addPrice = $payment->get('price'); + $surcharge = OrderCostEngine::calculatePaymentSurcharge($this->modx, $payment, $cost); - if (empty($addPrice)) { - return $cost; - } - - if (PriceAdjustment::isPercent($addPrice)) { - $percent = PriceAdjustment::getPercent($addPrice); - if (!PriceAdjustment::isAllowedPercent($percent)) { - $this->modx->log( - modX::LOG_LEVEL_ERROR, - sprintf( - '[Payment] Invalid percent value for payment #%s: %s%%. Must be between -100%% and 100%%.', - $payment->get('id'), - $percent - ) - ); - return $cost; - } - } - - return $cost + PriceAdjustment::calculate($cost, $addPrice); + return OrderCostEngine::calculatePaymentTotal($cost, $surcharge); } /** diff --git a/core/components/minishop3/src/ServiceRegistry.php b/core/components/minishop3/src/ServiceRegistry.php index 04980615..c7a87747 100644 --- a/core/components/minishop3/src/ServiceRegistry.php +++ b/core/components/minishop3/src/ServiceRegistry.php @@ -93,6 +93,10 @@ class ServiceRegistry 'class' => \MiniShop3\Services\Order\OrderCostCalculator::class, 'interface' => null, ], + 'ms3_manager_order_cost_recalculator' => [ + 'class' => \MiniShop3\Services\Order\ManagerOrderCostRecalculator::class, + 'interface' => null, + ], 'ms3_order_field_manager' => [ 'class' => \MiniShop3\Services\Order\OrderFieldManager::class, 'interface' => null, @@ -453,6 +457,7 @@ protected function registerService(string $serviceKey, array $config): bool $servicesWithModxAndMs3 = [ 'ms3_order_draft_manager', 'ms3_order_cost_calculator', + 'ms3_manager_order_cost_recalculator', 'ms3_order_user_resolver', 'ms3_order_log', 'ms3_cart_item_manager', diff --git a/core/components/minishop3/src/Services/Order/ManagerOrderCostRecalculator.php b/core/components/minishop3/src/Services/Order/ManagerOrderCostRecalculator.php index 4a5f2b9c..b0836b51 100644 --- a/core/components/minishop3/src/Services/Order/ManagerOrderCostRecalculator.php +++ b/core/components/minishop3/src/Services/Order/ManagerOrderCostRecalculator.php @@ -5,7 +5,6 @@ use MiniShop3\Controllers\Delivery\DefaultDelivery; use MiniShop3\Controllers\Payment\DefaultPayment; use MiniShop3\MiniShop3; -use MiniShop3\Utils\PriceAdjustment; use MiniShop3\Model\msDelivery; use MiniShop3\Model\msOrder; use MiniShop3\Model\msOrderLog; @@ -17,6 +16,9 @@ * Explicit manager-side recomputation of order totals from persisted msOrderProducts * and configured delivery/payment methods (without mutating unrelated order fields). * + * Manager adapter: modes (auto/manual/force_provider), warnings, order log — no msOn* cost events. + * Default-handler formulas delegate to {@see OrderCostEngine} (shared with web checkout). + * * External delivery/payment provider classes are not invoked in {@see self::MODE_AUTO}; * callers should use manual delivery cost or {@see self::MODE_FORCE_PROVIDER}. * @@ -87,7 +89,7 @@ public function recalculate(msOrder $order, array $options = []): array $deliveryCost = $deliveryResult['delivery_cost']; - $paymentBase = round($cartCost + $deliveryCost, 6); + $paymentBase = OrderCostEngine::paymentCommissionBase($cartCost, $deliveryCost); $paymentResult = $this->resolvePaymentFee($order, $paymentBase, $mode, $options); if (!$paymentResult['success']) { return $paymentResult; @@ -99,7 +101,8 @@ public function recalculate(msOrder $order, array $options = []): array /** @var OrderService $orderService */ $orderService = $this->modx->services->get('ms3_order_service'); - $cost = round($orderService->clampComputedTotal($order, $cartCost, $deliveryCost, $paymentFee), 6); + $breakdown = OrderCostEngine::composeBreakdown($order, $orderService, $cartCost, $deliveryCost, $paymentFee); + $cost = $breakdown['cost']; $before = [ 'cart_cost' => (float)$order->get('cart_cost'), @@ -221,7 +224,12 @@ protected function resolveDeliveryCost( if ($this->isSimpleDelivery($msDelivery)) { return [ 'success' => true, - 'delivery_cost' => $this->calculateDefaultDeliveryCost($msDelivery, $cartCost, $orderWeight), + 'delivery_cost' => OrderCostEngine::calculateDefaultDeliveryCost( + $this->modx, + $msDelivery, + $cartCost, + $orderWeight + ), 'warnings' => $warnings, ]; } @@ -300,7 +308,7 @@ protected function resolvePaymentFee(msOrder $order, float $paymentBase, string if ($this->isSimplePayment($msPayment)) { return [ 'success' => true, - 'payment_fee' => $this->calculateDefaultPaymentCommission($msPayment, $paymentBase), + 'payment_fee' => OrderCostEngine::calculatePaymentSurcharge($this->modx, $msPayment, $paymentBase), 'warnings' => $warnings, ]; } @@ -314,81 +322,6 @@ protected function resolvePaymentFee(msOrder $order, float $paymentBase, string ]; } - protected function calculateDefaultDeliveryCost(msDelivery $delivery, float $cartCost, float $orderWeight): float - { - $freeDeliveryAmount = (float)$delivery->get('free_delivery_amount'); - - if ($freeDeliveryAmount > 0 && $cartCost >= $freeDeliveryAmount) { - return 0.0; - } - - $deliveryCost = 0.0; - $weightPrice = (float)$delivery->get('weight_price'); - if ($weightPrice < 0) { - $this->modx->log( - modX::LOG_LEVEL_ERROR, - '[ManagerOrderCostRecalculator] Invalid weight_price for delivery #' . $delivery->get( - 'id' - ) . ': ' . $weightPrice, - ); - $weightPrice = 0; - } - - $deliveryCost += $weightPrice * $orderWeight; - - $addPrice = $delivery->get('price'); - if (empty($addPrice)) { - return round($deliveryCost, 6); - } - - if (PriceAdjustment::isPercent($addPrice)) { - $percent = PriceAdjustment::getPercent($addPrice); - if (!PriceAdjustment::isAllowedPercent($percent)) { - $this->modx->log( - modX::LOG_LEVEL_ERROR, - sprintf( - '[ManagerOrderCostRecalculator] Invalid percent for delivery #%s: %s%%. Must be between -100%% and 100%%.', - $delivery->get('id'), - $percent - ) - ); - - return round($deliveryCost, 6); - } - } - - return round($deliveryCost + PriceAdjustment::calculate($cartCost, $addPrice), 6); - } - - /** - * Surcharge only (excluding base), aligned with {@see \MiniShop3\Controllers\Payment\Payment::getCost()}. - */ - protected function calculateDefaultPaymentCommission(msPayment $payment, float $baseCost): float - { - $addPrice = $payment->get('price'); - if (empty($addPrice)) { - return 0.0; - } - - if (PriceAdjustment::isPercent($addPrice)) { - $percent = PriceAdjustment::getPercent($addPrice); - if (!PriceAdjustment::isAllowedPercent($percent)) { - $this->modx->log( - modX::LOG_LEVEL_ERROR, - sprintf( - '[ManagerOrderCostRecalculator] Invalid percent for payment #%s: %s%%. Must be between -100%% and 100%%.', - $payment->get('id'), - $percent - ) - ); - - return 0.0; - } - } - - return round(PriceAdjustment::calculate($baseCost, $addPrice), 6); - } - protected function isSimpleDelivery(msDelivery $delivery): bool { return $this->isDefaultShippingPaymentHandlerClass((string) $delivery->get('class'), DefaultDelivery::class); diff --git a/core/components/minishop3/src/Services/Order/OrderCostCalculator.php b/core/components/minishop3/src/Services/Order/OrderCostCalculator.php index 7e557394..4101f4f7 100644 --- a/core/components/minishop3/src/Services/Order/OrderCostCalculator.php +++ b/core/components/minishop3/src/Services/Order/OrderCostCalculator.php @@ -11,8 +11,12 @@ /** * Order Cost Calculator * - * Calculates order costs: cart, delivery, payment, and total. - * Supports events for cost modification by plugins. + * Web/checkout adapter: cart status, delivery/payment providers, MODX cost events. + * Core formulas live in {@see OrderCostEngine}; manager path uses {@see ManagerOrderCostRecalculator}. + * + * Events (web-only): msOnBeforeGetCartCost, msOnGetCartCost, msOnBeforeGetDeliveryCost, + * msOnGetDeliveryCost, msOnBeforeGetPaymentCost, msOnGetPaymentCost, + * msOnBeforeGetOrderCost, msOnGetOrderCost. */ class OrderCostCalculator { @@ -151,10 +155,18 @@ public function getDeliveryCost(?msOrder $draft, array $orderData, string $token * @param array $orderData Order data array (with payment_id) * @param string $token Session token * @param string $ctx Context + * @param float|null $knownCartCost Skip cart pipeline when already computed (e.g. from {@see getTotalCost}) + * @param float|null $knownDeliveryCost Skip delivery pipeline when already computed * @return array Response with 'cost' key */ - public function getPaymentCost(?msOrder $draft, array $orderData, string $token, string $ctx = 'web'): array - { + public function getPaymentCost( + ?msOrder $draft, + array $orderData, + string $token, + string $ctx = 'web', + ?float $knownCartCost = null, + ?float $knownDeliveryCost = null + ): array { // No draft = no order = zero payment cost if (!$draft) { return $this->success('ms3_order_getcost_success', ['cost' => 0]); @@ -186,13 +198,22 @@ public function getPaymentCost(?msOrder $draft, array $orderData, string $token, return $this->success('ms3_order_getcost_success', ['cost' => $paymentCost]); } - // Get cart cost for payment calculation - $cartCostResponse = $this->getCartCost($draft, $token, $ctx); - $cartCost = $cartCostResponse['success'] ? $cartCostResponse['data']['cost'] : 0; + // Payment commission base = cart + delivery (#372 / #366) + if ($knownCartCost === null) { + $cartCostResponse = $this->getCartCost($draft, $token, $ctx); + $knownCartCost = $cartCostResponse['success'] ? (float) $cartCostResponse['data']['cost'] : 0.0; + } + + if ($knownDeliveryCost === null) { + $deliveryCostResponse = $this->getDeliveryCost($draft, $orderData, $token, $ctx); + $knownDeliveryCost = $deliveryCostResponse['success'] ? (float) $deliveryCostResponse['data']['cost'] : 0.0; + } - // Payment getCost returns total with payment fee, so subtract cart cost - $costWithPayment = $msPayment->getCost($draft, $cartCost); - $paymentCost = $costWithPayment - $cartCost; + $paymentBase = OrderCostEngine::paymentCommissionBase($knownCartCost, $knownDeliveryCost); + + // Payment getCost returns total with payment fee, so subtract commission base + $costWithPayment = $msPayment->getCost($draft, $paymentBase); + $paymentCost = $costWithPayment - $paymentBase; $response = $this->ms3->utils->invokeEvent('msOnGetPaymentCost', [ 'calculator' => $this, @@ -239,22 +260,25 @@ public function getTotalCost( } $cartCostResponse = $this->getCartCost($draft, $token, $ctx); - $cartCost = $cartCostResponse['success'] ? $cartCostResponse['data']['cost'] : 0; + $cartCost = $cartCostResponse['success'] ? (float) $cartCostResponse['data']['cost'] : 0.0; $deliveryCostResponse = $this->getDeliveryCost($draft, $orderData, $token, $ctx); - $deliveryCost = $deliveryCostResponse['success'] ? $deliveryCostResponse['data']['cost'] : 0; + $deliveryCost = $deliveryCostResponse['success'] ? (float) $deliveryCostResponse['data']['cost'] : 0.0; - $paymentCostResponse = $this->getPaymentCost($draft, $orderData, $token, $ctx); - $paymentCost = $paymentCostResponse['success'] ? $paymentCostResponse['data']['cost'] : 0; + $paymentCostResponse = $this->getPaymentCost( + $draft, + $orderData, + $token, + $ctx, + $cartCost, + $deliveryCost + ); + $paymentCost = $paymentCostResponse['success'] ? (float) $paymentCostResponse['data']['cost'] : 0.0; /** @var OrderService $orderService */ $orderService = $this->modx->services->get('ms3_order_service'); - $cost = $orderService->clampComputedTotal( - $draft, - (float) $cartCost, - (float) $deliveryCost, - (float) $paymentCost - ); + $breakdown = OrderCostEngine::composeBreakdown($draft, $orderService, $cartCost, $deliveryCost, $paymentCost); + $cost = $breakdown['cost']; $after = $this->ms3->utils->invokeEvent('msOnGetOrderCost', [ 'calculator' => $this, @@ -275,7 +299,8 @@ public function getTotalCost( $cartCost = (float) ($after['data']['cart_cost'] ?? $cartCost); $deliveryCost = (float) ($after['data']['delivery_cost'] ?? $deliveryCost); $paymentCost = (float) ($after['data']['payment_cost'] ?? $paymentCost); - $cost = $orderService->clampComputedTotal($draft, $cartCost, $deliveryCost, $paymentCost); + $breakdown = OrderCostEngine::composeBreakdown($draft, $orderService, $cartCost, $deliveryCost, $paymentCost); + $cost = $breakdown['cost']; if ($onlyCost) { return $this->success('ms3_order_getcost_success', ['cost' => $cost]); diff --git a/core/components/minishop3/src/Services/Order/OrderCostEngine.php b/core/components/minishop3/src/Services/Order/OrderCostEngine.php new file mode 100644 index 00000000..8ea1ca64 --- /dev/null +++ b/core/components/minishop3/src/Services/Order/OrderCostEngine.php @@ -0,0 +1,139 @@ +get('free_delivery_amount'); + + if ($freeDeliveryAmount > 0 && $cartCost >= $freeDeliveryAmount) { + return 0.0; + } + + $deliveryCost = 0.0; + $weightPrice = (float) $delivery->get('weight_price'); + if ($weightPrice < 0) { + $modx->log( + modX::LOG_LEVEL_ERROR, + '[OrderCostEngine] Invalid weight_price for delivery #' . $delivery->get('id') . ': ' . $weightPrice + ); + $weightPrice = 0.0; + } + + $deliveryCost += $weightPrice * $orderWeight; + + $addPrice = $delivery->get('price'); + if (empty($addPrice)) { + return round($deliveryCost, 6); + } + + if (PriceAdjustment::isPercent($addPrice)) { + $percent = PriceAdjustment::getPercent($addPrice); + if (!PriceAdjustment::isAllowedPercent($percent)) { + $modx->log( + modX::LOG_LEVEL_ERROR, + sprintf( + '[OrderCostEngine] Invalid percent for delivery #%s: %s%%. Must be between -100%% and 100%%.', + $delivery->get('id'), + $percent + ) + ); + + return round($deliveryCost, 6); + } + } + + return round($deliveryCost + PriceAdjustment::calculate($cartCost, $addPrice), 6); + } + + /** + * Payment surcharge only (excluding commission base), aligned with {@see \MiniShop3\Controllers\Payment\Payment::getCost()}. + */ + public static function calculatePaymentSurcharge(modX $modx, msPayment $payment, float $baseCost): float + { + $addPrice = $payment->get('price'); + if (empty($addPrice)) { + return 0.0; + } + + if (PriceAdjustment::isPercent($addPrice)) { + $percent = PriceAdjustment::getPercent($addPrice); + if (!PriceAdjustment::isAllowedPercent($percent)) { + $modx->log( + modX::LOG_LEVEL_ERROR, + sprintf( + '[OrderCostEngine] Invalid percent for payment #%s: %s%%. Must be between -100%% and 100%%.', + $payment->get('id'), + $percent + ) + ); + + return 0.0; + } + } + + return round(PriceAdjustment::calculate($baseCost, $addPrice), 6); + } + + public static function calculatePaymentTotal(float $baseCost, float $surcharge): float + { + return round($baseCost + $surcharge, 6); + } + + /** + * @return array{cart_cost: float, delivery_cost: float, payment_cost: float, cost: float} + */ + public static function composeBreakdown( + ?msOrder $order, + OrderService $orderService, + float $cartCost, + float $deliveryCost, + float $paymentSurcharge + ): array { + $cartCost = round($cartCost, 6); + $deliveryCost = round($deliveryCost, 6); + $paymentCost = round($paymentSurcharge, 6); + $cost = round( + $orderService->clampComputedTotal($order, $cartCost, $deliveryCost, $paymentCost), + 6 + ); + + return [ + 'cart_cost' => $cartCost, + 'delivery_cost' => $deliveryCost, + 'payment_cost' => $paymentCost, + 'cost' => $cost, + ]; + } +} diff --git a/core/components/minishop3/tests/OrderCostEngineTest.php b/core/components/minishop3/tests/OrderCostEngineTest.php new file mode 100644 index 00000000..7fea457b --- /dev/null +++ b/core/components/minishop3/tests/OrderCostEngineTest.php @@ -0,0 +1,147 @@ + 0.000001) { + $fail($case . ': expected ' . $expected . ', got ' . $actual); + } +}; + +$modx = new modX(); + +$assertFloat(150.0, OrderCostEngine::paymentCommissionBase(100.0, 50.0), 'payment base cart+delivery'); +$assertFloat(154.5, OrderCostEngine::calculatePaymentTotal(150.0, 4.5), 'payment total'); + +// #372: payment % base = cart + delivery (1000 + 500 → 3% fee = 45, not 30 on cart-only) +$issue372Base = OrderCostEngine::paymentCommissionBase(1000.0, 500.0); +$assertFloat(1500.0, $issue372Base, '#372 commission base'); +$assertFloat(45.0, PriceAdjustment::calculate($issue372Base, '3%'), '#372 payment surcharge on cart+delivery'); + +// Delivery free-threshold edge cases (#366 review). +// free_delivery_amount = 1000: cart at or above threshold → delivery is free. +$freeDelivery = new StubMsDelivery([ + 'id' => 1, + 'free_delivery_amount' => 1000.0, + 'weight_price' => 10.0, + 'price' => 0, +]); +$assertFloat(0.0, OrderCostEngine::calculateDefaultDeliveryCost($modx, $freeDelivery, 1000.0, 5.0), 'free delivery at threshold'); +$assertFloat(0.0, OrderCostEngine::calculateDefaultDeliveryCost($modx, $freeDelivery, 1500.0, 5.0), 'free delivery above threshold'); + +// Just below threshold: weight_price × weight + price (10×5 + 50 = 100). +$paidDelivery = new StubMsDelivery([ + 'id' => 2, + 'free_delivery_amount' => 1000.0, + 'weight_price' => 10.0, + 'price' => 50.0, +]); +$assertFloat(100.0, OrderCostEngine::calculateDefaultDeliveryCost($modx, $paidDelivery, 999.99, 5.0), 'paid delivery weight + fixed price below threshold'); + +// Percent delivery price applied to cart (5% of 1000 = 50), no weight component. +$percentDelivery = new StubMsDelivery([ + 'id' => 3, + 'free_delivery_amount' => 0, + 'weight_price' => 0.0, + 'price' => '5%', +]); +$assertFloat(50.0, OrderCostEngine::calculateDefaultDeliveryCost($modx, $percentDelivery, 1000.0, 0.0), 'percent delivery on cart'); + +// free_delivery_amount = 0 disables the free threshold (price-only delivery). +$noFreeThreshold = new StubMsDelivery([ + 'id' => 4, + 'free_delivery_amount' => 0, + 'weight_price' => 0.0, + 'price' => 75.0, +]); +$assertFloat(75.0, OrderCostEngine::calculateDefaultDeliveryCost($modx, $noFreeThreshold, 100000.0, 0.0), 'fixed delivery with no free threshold'); + +// Payment percent surcharge base = cart + delivery (#372): 3% of (1000 + 500) = 45. +$percentPayment = new StubMsPayment(['id' => 10, 'price' => '3%']); +$percentBase = OrderCostEngine::paymentCommissionBase(1000.0, 500.0); +$percentSurcharge = OrderCostEngine::calculatePaymentSurcharge($modx, $percentPayment, $percentBase); +$assertFloat(45.0, $percentSurcharge, 'payment 3% surcharge on cart+delivery base'); +$assertFloat(1545.0, OrderCostEngine::calculatePaymentTotal($percentBase, $percentSurcharge), 'payment total with percent on cart+delivery'); + +// Fixed payment surcharge is independent of the base. +$fixedPayment = new StubMsPayment(['id' => 11, 'price' => 30]); +$assertFloat(30.0, OrderCostEngine::calculatePaymentSurcharge($modx, $fixedPayment, 1500.0), 'fixed payment surcharge'); + +// Empty payment price → zero surcharge regardless of base. +$noPricePayment = new StubMsPayment(['id' => 12, 'price' => 0]); +$assertFloat(0.0, OrderCostEngine::calculatePaymentSurcharge($modx, $noPricePayment, 1500.0), 'zero payment surcharge when price empty'); + +$paths = [ + 'ManagerOrderCostRecalculator.php' => [ + 'OrderCostEngine::paymentCommissionBase', + 'OrderCostEngine::composeBreakdown', + 'OrderCostEngine::calculateDefaultDeliveryCost', + 'OrderCostEngine::calculatePaymentSurcharge', + ], + 'OrderCostCalculator.php' => [ + 'OrderCostEngine::paymentCommissionBase', + ], + 'Delivery.php' => [ + 'OrderCostEngine::calculateDefaultDeliveryCost', + ], + 'Payment.php' => [ + 'OrderCostEngine::calculatePaymentSurcharge', + 'OrderCostEngine::calculatePaymentTotal', + ], +]; + +foreach ($paths as $file => $needles) { + $src = file_get_contents(__DIR__ . '/../src/' . ($file === 'Delivery.php' || $file === 'Payment.php' + ? 'Controllers/' . ($file === 'Delivery.php' ? 'Delivery/Delivery.php' : 'Payment/Payment.php') + : 'Services/Order/' . $file)); + if ($src === false) { + $fail("unable to read {$file}"); + } + foreach ($needles as $needle) { + if (!str_contains($src, $needle)) { + $fail("{$file} must reference {$needle}"); + } + } +} + +$registry = file_get_contents(__DIR__ . '/../src/ServiceRegistry.php'); +if ($registry === false || !str_contains($registry, 'ms3_manager_order_cost_recalculator')) { + $fail('ServiceRegistry must register ms3_manager_order_cost_recalculator'); +} + +$ordersCtrl = file_get_contents(__DIR__ . '/../src/Controllers/Api/Manager/OrdersController.php'); +if ($ordersCtrl === false || !str_contains($ordersCtrl, "services->get('ms3_manager_order_cost_recalculator')")) { + $fail('OrdersController must resolve manager recalculator from DI'); +} + +$calcSrc = file_get_contents(__DIR__ . '/../src/Services/Order/OrderCostCalculator.php'); +if ($calcSrc === false || !preg_match('/getPaymentCost\([^)]+\$cartCost,\s*\$deliveryCost/', $calcSrc)) { + $fail('getTotalCost must pass precomputed cart/delivery into getPaymentCost (single pipeline pass)'); +} + +fwrite(STDOUT, "OK OrderCostEngineTest\n"); +exit(0); diff --git a/core/components/minishop3/tests/stubs/StubMsDelivery.php b/core/components/minishop3/tests/stubs/StubMsDelivery.php new file mode 100644 index 00000000..7c5c4c43 --- /dev/null +++ b/core/components/minishop3/tests/stubs/StubMsDelivery.php @@ -0,0 +1,26 @@ + */ + private array $fields; + + /** + * @param array $fields + */ + public function __construct(array $fields = []) + { + $this->fields = $fields; + } + + public function get($key) + { + return $this->fields[$key] ?? null; + } +}