Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1396,21 +1396,15 @@ public function deleteProduct(array $params = []): array
*/
protected function recalculateOrderTotals(msOrder $order): void
{
$cartCost = 0;
$weight = 0;

$products = $this->modx->getIterator(\MiniShop3\Model\msOrderProduct::class, [
'order_id' => $order->get('id'),
]);

foreach ($products as $product) {
$cartCost += (float)$product->get('cost');
$weight += (float)$product->get('weight') * (int)$product->get('count');
}
$totals = OrderService::aggregateProductsTotals($products);
$cartCost = $totals['cart_cost'];
$weight = $totals['weight'];

$order->set('cart_cost', $cartCost);
$order->set('weight', $weight);

// Recalculate total cost (cart + delivery; payment deltas are reflected in cost when persisted elsewhere)
/** @var OrderService $orderService */
$orderService = $this->modx->services->get('ms3_order_service');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,22 +48,11 @@ public function __construct(modX $modx, MiniShop3 $ms3)
*/
public function calculateProductTotals(msOrder $order): array
{
$cartCost = 0.0;
$weight = 0.0;

$products = $this->modx->getIterator(msOrderProduct::class, [
'order_id' => $order->get('id'),
]);

foreach ($products as $product) {
$cartCost += (float)$product->get('cost');
$weight += (float)$product->get('weight') * (int)$product->get('count');
}

return [
'cart_cost' => round($cartCost, 6),
'weight' => round($weight, 6),
];
return OrderService::aggregateProductsTotals($products);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,17 @@ public function getTotalCost(
string $ctx = 'web',
bool $onlyCost = false
): array {
$before = $this->ms3->utils->invokeEvent('msOnBeforeGetOrderCost', [
'calculator' => $this,
'cart' => $this->ms3->cart,
'draft' => $draft,
'with_cart' => true,
'only_cost' => $onlyCost,
]);
if (!$before['success']) {
return $this->error($before['message']);
}

$cartCostResponse = $this->getCartCost($draft, $token, $ctx);
$cartCost = $cartCostResponse['success'] ? $cartCostResponse['data']['cost'] : 0;

Expand All @@ -245,6 +256,27 @@ public function getTotalCost(
(float) $paymentCost
);

$after = $this->ms3->utils->invokeEvent('msOnGetOrderCost', [
'calculator' => $this,
'cart' => $this->ms3->cart,
'draft' => $draft,
'with_cart' => true,
'only_cost' => $onlyCost,
'cost' => $cost,
'cart_cost' => $cartCost,
'delivery_cost' => $deliveryCost,
'payment_cost' => $paymentCost,
]);
if (!$after['success']) {
return $this->error($after['message']);
}

$cost = (float) ($after['data']['cost'] ?? $cost);
$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);

if ($onlyCost) {
return $this->success('ms3_order_getcost_success', ['cost' => $cost]);
}
Expand Down
21 changes: 8 additions & 13 deletions core/components/minishop3/src/Services/Order/OrderDraftManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,28 +145,23 @@ public function getOrCreateDraft(string $token, string $ctx = 'web', ?int $custo
}

/**
* Recalculate order costs (cart, delivery, total)
* Recalculate and persist draft cart_cost / weight / cost from order products.
*
* Plugin hooks: $draft->save() fires msOnBeforeSaveOrder / msOnSaveOrder (see msOrder::save).
* Display-time cost adjustment uses msOnBeforeGetOrderCost / msOnGetOrderCost via OrderCostCalculator,
* not this persist path — do not invent a separate "recalculate" event here.
*/
public function recalculate(msOrder $draft): void
{
// TODO: event before recalculating order
$products = $draft->getMany('Products');
$cart_cost = 0;
$weight = 0;

if (!empty($products)) {
foreach ($products as $product) {
$weight += $product->get('weight');
$cart_cost += $product->get('cost');
}
}
$totals = OrderService::aggregateProductsTotals($draft->getMany('Products') ?? []);
$cart_cost = $totals['cart_cost'];
$weight = $totals['weight'];

/** @var OrderService $orderService */
$orderService = $this->modx->services->get('ms3_order_service');
$delivery_cost = (float) $draft->get('delivery_cost');
$cost = $orderService->clampComputedTotal($draft, (float) $cart_cost, $delivery_cost, 0.0);

// TODO: event on recalculating order
$draft->set('updatedon', time());
$draft->set('cart_cost', $cart_cost);
$draft->set('cost', $cost);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -380,18 +380,12 @@ protected function validateDeliveryRequiredFields(msOrder $order): array
*/
protected function calculateCosts(msOrder $order): array
{
// Calculate cart cost from order products
$cartCost = 0;
$weight = 0;

$products = $this->modx->getIterator(msOrderProduct::class, [
'order_id' => $order->get('id'),
]);

foreach ($products as $product) {
$cartCost += (float) $product->get('cost');
$weight += (float) $product->get('weight') * (int) $product->get('count');
}
$totals = OrderService::aggregateProductsTotals($products);
$cartCost = $totals['cart_cost'];
$weight = $totals['weight'];

// Calculate delivery cost
$deliveryCost = 0;
Expand Down
23 changes: 23 additions & 0 deletions core/components/minishop3/src/Services/Order/OrderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,29 @@ public function clampComputedTotal(
return 0.0;
}

/**
* Sum cart_cost and total weight from order product lines.
* Weight is unit weight × count (cart status / draft / finalize / manager).
*
* @param iterable<object> $products Objects with get('weight'|'cost'|'count')
* @return array{cart_cost: float, weight: float}
*/
public static function aggregateProductsTotals(iterable $products): array
{
$cartCost = 0.0;
$weight = 0.0;

foreach ($products as $product) {
$cartCost += (float) $product->get('cost');
$weight += (float) $product->get('weight') * (int) $product->get('count');
}

return [
'cart_cost' => round($cartCost, 6),
'weight' => round($weight, 6),
];
}

/**
* Recalculate products in order
*
Expand Down
75 changes: 75 additions & 0 deletions core/components/minishop3/tests/OrderDraftWeightTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

/**
* Regression: draft weight must multiply unit weight by count (#375).
*
* Run: php tests/OrderDraftWeightTest.php
*/

declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use MiniShop3\Services\Order\OrderService;

$fail = static function (string $message): never {
fwrite(STDERR, "FAIL: {$message}\n");
exit(1);
};

$assertSame = static function ($expected, $actual, string $label) use ($fail): void {
if ($actual !== $expected) {
$fail(sprintf(
"%s:\nexpected: %s\nactual: %s",
$label,
var_export($expected, true),
var_export($actual, true)
));
}
};

$product = static function (float $weight, int $count, float $cost = 0.0): object {
return new class ($weight, $count, $cost) {
public function __construct(
private float $weight,
private int $count,
private float $cost,
) {
}

public function get(string $field): float|int
{
return match ($field) {
'weight' => $this->weight,
'count' => $this->count,
'cost' => $this->cost,
default => 0,
};
}
};
};

$empty = OrderService::aggregateProductsTotals([]);
$assertSame(0.0, $empty['weight'], 'empty weight');
$assertSame(0.0, $empty['cart_cost'], 'empty cart_cost');

$single = OrderService::aggregateProductsTotals([
$product(1.0, 3, 30.0),
]);
$assertSame(3.0, $single['weight'], 'weight=1 count=3 → total weight 3');
$assertSame(30.0, $single['cart_cost'], 'line cost is not multiplied again');

$multi = OrderService::aggregateProductsTotals([
$product(1.5, 2, 10.0),
$product(0.5, 4, 20.0),
]);
$assertSame(5.0, $multi['weight'], '1.5*2 + 0.5*4');
$assertSame(30.0, $multi['cart_cost'], '10 + 20');

// Guard: unit weight alone must not be treated as total (the #375 bug)
if ($single['weight'] === 1.0) {
$fail('weight must include count; got unit weight only');
}

fwrite(STDOUT, "OK OrderDraftWeightTest\n");
exit(0);