Skip to content
Merged
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
9 changes: 8 additions & 1 deletion plugins/sCommercePlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,16 @@
});

/**
* Remember advertising tags (gclid, UTM) from the landing URL until checkout
* Remember advertising tags (gclid, UTM) from the landing URL until checkout.
*
* CAPTCHA and other API entrypoints can emit this event without an HTTP request
* binding. They must not resolve the request or initialize marketing services.
*/
Event::listen('evolution.OnWebPageInit', function() {
if (!app()->bound('request')) {
return;
}

sCommerce::captureMarketingTags(request()->query());
});

Expand Down
4 changes: 3 additions & 1 deletion src/Cart/sCart.php
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,8 @@ protected function saveCartData(): void
* Get details of a product by its ID with selected fields.
*
* This method retrieves detailed information about a product from the database.
* Physical values come from the product attribute API so cart and order snapshots
* retain delivery dimensions and measured-line weight after column migration.
*
* @param object $product The product.
* @throws \UnexpectedValueException If the returned object is not a product.
Expand All @@ -406,7 +408,7 @@ private function getProductFields(object $product, array $pricing = []): ?array
}
}

$productFields = array_merge($product->only($this->productDetails), $pricing);
$productFields = array_merge($product->only($this->productDetails), $pricing, $product->physicalPayload());

return array_merge($productFields, $attributes);
}
Expand Down
30 changes: 13 additions & 17 deletions src/Checkout/sCheckout.php
Original file line number Diff line number Diff line change
Expand Up @@ -644,8 +644,9 @@ public function processOrder(): array
*
* This method validates the input data, creates a new order for a quick purchase,
* and saves it in the database. If product is 0, it will fetch products from the cart.
* Quantities remain uncast for validation; integer conversion and single-product totals
* Piece quantities remain uncast for validation; integer conversion and single-product totals
* (including container deposits) are calculated only after the validator accepts them.
* Measured lines retain base-unit normalization and scaled-price totals after validation.
* The trusted checkout context is resolved before the order is built.
*
* @param array $data The data from the request, including productId, optionId, quantity, exchange (empty returnable
Expand Down Expand Up @@ -736,11 +737,6 @@ public static function quickOrder(array $data)
}
$pricing = static::resolveProductPricing($product, $optionId);

$price = (float)$pricing['priceAsFloat'];
$cost = $rules->isMeasured()
? MeasuredQuantity::lineTotalMinor(MeasuredQuantity::scalePrice($price), $quantity, $rules->factor) / 100
: $price * $quantity;

$productData = [
'id' => $product->id,
'title' => $product->title,
Expand All @@ -761,17 +757,7 @@ public static function quickOrder(array $data)
}
$productData = array_merge($productData, app(OrderLineIdentity::class)->snapshot($product, $optionId));

// Units exchanged for empty returnable containers carry no new deposit.
$productData = array_merge($productData, ContainerDeposit::lineFields(ContainerDeposit::unit($product, $optionId), $quantity, max(0, (int)($data['exchange'] ?? 0))));
$cost += (float)($productData['deposit_amount'] ?? 0);

$productData = array_merge($productData, $physical = $product->physicalPayload());
$productData += MeasuredQuantity::lineFields($rules, $quantity, (string)($pricing['price'] ?? ''), (int)($physical['weight'] ?? 0));
if ($rules->isMeasured()) {
$productData['sumAsFloat'] = round($cost, 3);
$productData['sum'] = sCommerce::convertPrice($productData['sumAsFloat']);
}

$productsData = [$productData];
}

Expand All @@ -785,12 +771,22 @@ public static function quickOrder(array $data)
if (!empty($data['productId']) && $data['productId'] != 0) {
$quantity = (int) $quantity;
$productsData[0]['quantity'] = $quantity;
$cost = (float) $pricing['priceAsFloat'] * $quantity;
$price = (float) $pricing['priceAsFloat'];
$cost = $rules->isMeasured()
? MeasuredQuantity::lineTotalMinor(MeasuredQuantity::scalePrice($price), $quantity, $rules->factor) / 100
: $price * $quantity;
// Calculate deposit amounts only after the raw quantity has passed validation.
$productsData[0] = array_merge($productsData[0], ContainerDeposit::lineFields(
ContainerDeposit::unit($product, $optionId), $quantity, max(0, (int)($data['exchange'] ?? 0))
));
$cost += (float) ($productsData[0]['deposit_amount'] ?? 0);
$productsData[0] += MeasuredQuantity::lineFields(
$rules, $quantity, (string)($pricing['price'] ?? ''), (int)($physical['weight'] ?? 0)
);
if ($rules->isMeasured()) {
$productsData[0]['sumAsFloat'] = round($cost, 3);
$productsData[0]['sum'] = sCommerce::convertPrice($productsData[0]['sumAsFloat']);
}
}

do {
Expand Down
53 changes: 53 additions & 0 deletions tests/standalone/scommerce-marketing-request.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

/** Return the isolated container used by CMS helpers in this regression. */
function evo() { return $GLOBALS['marketingTestContainer']; }

require __DIR__ . '/bootstrap.php';

use Illuminate\Container\Container;
use Illuminate\Events\Dispatcher;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Facade;

$container = new Container();
$GLOBALS['marketingTestContainer'] = $container;
$events = new Dispatcher($container);
$container->instance('events', $events);
Facade::setFacadeApplication($container);
require dirname(__DIR__, 2) . '/plugins/sCommercePlugin.php';

$checks = 0;
$check = static function (bool $condition, string $message) use (&$checks): void {
if (!$condition) { throw new RuntimeException($message); }
$checks++;
};

$marketingResolutions = 0;
$container->bind('sCommerce', static function () use (&$marketingResolutions) {
$marketingResolutions++;
throw new RuntimeException('Marketing service must not boot without a request');
});
$events->dispatch('evolution.OnWebPageInit');
$check($marketingResolutions === 0, 'Unbound request must skip marketing service');
$check(!$container->bound('request'), 'Listener must not synthesize a request');

/** Record listener payloads without writing cookies or loading commerce state. */
$receiver = new class {
public array $queries = [];
/** Record the unchanged query passed to the existing marketing API. */
public function captureMarketingTags(array $query): void { $this->queries[] = $query; }
};
$container->instance('sCommerce', $receiver);
$query = ['utm_source' => 'newsletter', 'gclid' => 'test-click', 'other' => 'preserve'];
$container->instance('request', Request::create('/baltics/', 'GET', $query));
$events->dispatch('evolution.OnWebPageInit');
$check($receiver->queries === [$query], 'Bound HTTP request must preserve the entire query');
$container->instance('request', Request::create('/baltics/'));
$events->dispatch('evolution.OnWebPageInit');
$check($receiver->queries === [$query, []], 'Empty query must retain existing capture behavior');

$container->forgetInstance('request');
$events->dispatch('evolution.OnWebPageInit');
$check(count($receiver->queries) === 2, 'Missing request must remain safe after earlier HTTP events');
echo "Marketing request checks passed: {$checks}\n";
2 changes: 1 addition & 1 deletion tests/standalone/scommerce-measured-quantities.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ function config($key = null, $default = null) { return app('config')->get($key,
$check(!array_intersect_key($byId[11], array_flip(array_merge(MeasuredQuantity::LINE_KEYS, MeasuredQuantity::DISPLAY_KEYS))), 'Piece lines have no measured keys');
$check($byId[13]['quantity'] === 2400 && $byId[13]['sumAsFloat'] === 23.76 && !isset($byId[13]['line_weight']) && $mini['totalSum'] === 3.04 + 25.0 + 23.76, 'Volume line without weight; totals add up');
$capture = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg(__DIR__ . '/fixtures/piece-cart-capture.php') . ' ' . escapeshellarg($package));
$check($capture === file_get_contents(__DIR__ . '/fixtures/piece-cart-master.json'), 'Piece cart and order JSON are byte-identical to master');
$check($capture === str_replace("\r\n", "\n", file_get_contents(__DIR__ . '/fixtures/piece-cart-master.json')), 'Piece cart and order JSON are byte-identical to master (normalized line endings)');

// 10. Quick order and checkout keep the snapshot keys and the exact total.
$quick = sCheckout::quickOrder(['productId' => 10, 'amount' => '2.53', 'email' => 'quick@shop.test']);
Expand Down
Loading