From fd4fec842ee6478250bb0b7e7bb8f48bafcc479c Mon Sep 17 00:00:00 2001 From: GonGarce Date: Sat, 13 Jun 2026 09:59:40 +0200 Subject: [PATCH 1/2] Improve and fix float truncation on weight rate shipping --- .../resources/lang/en/relationmanagers.php | 4 +- .../src/Drivers/ShippingMethods/ShipBy.php | 7 +- .../Pages/ManageShippingRates.php | 24 ++- .../Drivers/ShippingMethods/ShipByTest.php | 8 +- .../ShippingMethods/ShipByWeightUnitTest.php | 199 ++++++++++++++++++ 5 files changed, 227 insertions(+), 15 deletions(-) create mode 100644 tests/shipping/Unit/Drivers/ShippingMethods/ShipByWeightUnitTest.php diff --git a/packages/table-rate-shipping/resources/lang/en/relationmanagers.php b/packages/table-rate-shipping/resources/lang/en/relationmanagers.php index f0790ab92a..7cb72eb7a8 100644 --- a/packages/table-rate-shipping/resources/lang/en/relationmanagers.php +++ b/packages/table-rate-shipping/resources/lang/en/relationmanagers.php @@ -38,8 +38,8 @@ 'label' => 'Min. Spend', ], 'min_weight' => [ - 'label' => 'Min. Weight', - 'helper_text' => 'Enter weight in kilograms', + 'label' => 'Min. Weight (:unit)', + 'helper_text' => 'Enter weight in :unit', ], 'price' => [ 'label' => 'Price', diff --git a/packages/table-rate-shipping/src/Drivers/ShippingMethods/ShipBy.php b/packages/table-rate-shipping/src/Drivers/ShippingMethods/ShipBy.php index f8282476e4..4abba9babf 100644 --- a/packages/table-rate-shipping/src/Drivers/ShippingMethods/ShipBy.php +++ b/packages/table-rate-shipping/src/Drivers/ShippingMethods/ShipBy.php @@ -71,9 +71,10 @@ public function resolve(ShippingOptionRequest $shippingOptionRequest): ?Shipping $tier = $subTotal; if ($chargeBy == 'weight') { - $tier = $cart->lines->sum( - fn ($line) => $line->purchasable->weight->to('weight.kg')->convert()->getValue() * $line->quantity - ); + $weightUnit = $data['weight_unit'] ?? 'kg'; + $tier = (int) round($cart->lines->sum( + fn ($line) => $line->purchasable->weight->to("weight.{$weightUnit}")->convert()->getValue() * $line->quantity + ) * 100); } // Do we have a suitable tier price? diff --git a/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingRates.php b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingRates.php index b99f4cb490..2c201e2afe 100644 --- a/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingRates.php +++ b/packages/table-rate-shipping/src/Filament/Resources/ShippingZoneResource/Pages/ManageShippingRates.php @@ -118,15 +118,14 @@ function () { ->required(), Forms\Components\TextInput::make('min_quantity') ->label(fn (Get $get) => static::isWeightCharge($get) - ? __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.repeater.min_weight.label') + ? __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.repeater.min_weight.label', ['unit' => strtoupper(static::getShippingWeightUnit($get('../../shipping_method_id')))]) : __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.repeater.min_spend.label') ) ->helperText(fn (Get $get) => static::isWeightCharge($get) - ? __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.repeater.min_weight.helper_text') + ? __('lunarpanel.shipping::relationmanagers.shipping_rates.form.prices.repeater.min_weight.helper_text', ['unit' => strtoupper(static::getShippingWeightUnit($get('../../shipping_method_id')))]) : null ) - // Unit symbol — intentionally not translated. - ->suffix(fn (Get $get) => static::isWeightCharge($get) ? 'kg' : null) + ->suffix(fn (Get $get) => static::isWeightCharge($get) ? static::getShippingWeightUnit($get('../../shipping_method_id')) : null) ->numeric() ->required(), ])->afterStateHydrated( @@ -143,7 +142,7 @@ static function (Forms\Components\Repeater $component, ?Model $record = null): v 'customer_group_id' => $price->customer_group_id, 'price' => $price->price->decimal, 'currency_id' => $price->currency_id, - 'min_quantity' => $chargeBy == 'cart_total' ? $price->min_quantity / $currency->factor : $price->min_quantity, + 'min_quantity' => $chargeBy == 'cart_total' ? $price->min_quantity / $currency->factor : $price->min_quantity / 100, ]; })->toArray() ); @@ -225,6 +224,19 @@ private static function getShippingChargeBy(ShippingMethodContract|int|null $met return ($method?->data['charge_by'] ?? null) ?? 'cart_total'; } + private static function getShippingWeightUnit(ShippingMethodContract|int|null $method): string + { + if (blank($method)) { + return 'kg'; + } + + if (! $method instanceof ShippingMethodContract) { + $method = ShippingMethod::find($method); + } + + return ($method?->data['weight_unit'] ?? null) ?? 'kg'; + } + private static function isWeightCharge(Get $get): bool { return static::getShippingChargeBy($get('../../shipping_method_id')) === 'weight'; @@ -265,7 +277,7 @@ function ($price) use ($chargeBy, $currencies) { if ($chargeBy == 'cart_total') { $price['min_quantity'] = (int) ($price['min_quantity'] * $currency->factor); } else { - $price['min_quantity'] = (int) $price['min_quantity']; + $price['min_quantity'] = (int) ($price['min_quantity'] * 100); } $price['price'] = (int) ($price['price'] * $currency->factor); diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php index fb7a4a8d07..e1910a20fb 100644 --- a/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php +++ b/tests/shipping/Unit/Drivers/ShippingMethods/ShipByTest.php @@ -428,8 +428,8 @@ $shippingRate->prices()->createMany([ ['price' => 1000, 'min_quantity' => 1, 'currency_id' => $currency->id], - ['price' => 600, 'min_quantity' => 5, 'currency_id' => $currency->id], - ['price' => 200, 'min_quantity' => 10, 'currency_id' => $currency->id], + ['price' => 600, 'min_quantity' => 500, 'currency_id' => $currency->id], + ['price' => 200, 'min_quantity' => 1000, 'currency_id' => $currency->id], ]); $makeWeightCart = function (float $weightKg) use ($currency): Cart { @@ -497,7 +497,7 @@ $shippingRate->prices()->createMany([ ['price' => 1000, 'min_quantity' => 1, 'currency_id' => $currency->id], - ['price' => 500, 'min_quantity' => 5, 'currency_id' => $currency->id], + ['price' => 500, 'min_quantity' => 500, 'currency_id' => $currency->id], ]); $variant = ProductVariant::factory()->create([ @@ -549,7 +549,7 @@ $shippingRate->prices()->createMany([ ['price' => 1000, 'min_quantity' => 1, 'currency_id' => $currency->id], - ['price' => 400, 'min_quantity' => 5, 'currency_id' => $currency->id], + ['price' => 400, 'min_quantity' => 500, 'currency_id' => $currency->id], ]); $variantKg = ProductVariant::factory()->create(['weight_value' => 2.0, 'weight_unit' => 'kg']); diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/ShipByWeightUnitTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/ShipByWeightUnitTest.php new file mode 100644 index 0000000000..0c11966988 --- /dev/null +++ b/tests/shipping/Unit/Drivers/ShippingMethods/ShipByWeightUnitTest.php @@ -0,0 +1,199 @@ +group('shipping', 'shipping-driver', 'shipping-driver-shipby'); + +uses(RefreshDatabase::class); +uses(TestUtils::class); + +test('weight-based shipping uses configurable weight_unit from shipping method data', function () { + $currency = Currency::factory()->create(['default' => true]); + TaxClass::factory()->create(['default' => true]); + + $shippingZone = ShippingZone::factory()->create(['type' => 'countries']); + + $shippingMethod = ShippingMethod::factory()->create([ + 'driver' => 'ship-by', + 'data' => [ + 'charge_by' => 'weight', + 'weight_unit' => 'g', + ], + ]); + + $shippingRate = ShippingRate::factory()->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ + ['price' => 1000, 'min_quantity' => 1, 'currency_id' => $currency->id], + ['price' => 500, 'min_quantity' => 500, 'currency_id' => $currency->id], + ]); + + $variant = ProductVariant::factory()->create([ + 'weight_value' => 300.0, + 'weight_unit' => 'g', + ]); + $variant->stock = 100; + + Price::factory()->create([ + 'price' => 500, + 'min_quantity' => 1, + 'currency_id' => $currency->id, + 'priceable_type' => $variant->getMorphClass(), + 'priceable_id' => $variant->id, + ]); + + $cart = Cart::factory()->create(['currency_id' => $currency->id]); + $cart->lines()->create([ + 'purchasable_type' => $variant->getMorphClass(), + 'purchasable_id' => $variant->id, + 'quantity' => 2, + ]); + $cart = $cart->calculate(); + + $option = (new ShipBy)->resolve(new ShippingOptionRequest( + shippingRate: $shippingRate, + cart: $cart, + )); + + expect($option)->toBeInstanceOf(ShippingOption::class) + ->and($option->price->value)->toEqual(500); +}); + +test('weight-based shipping defaults to kg when weight_unit is not set', function () { + $currency = Currency::factory()->create(['default' => true]); + TaxClass::factory()->create(['default' => true]); + + $shippingZone = ShippingZone::factory()->create(['type' => 'countries']); + + $shippingMethod = ShippingMethod::factory()->create([ + 'driver' => 'ship-by', + 'data' => [ + 'charge_by' => 'weight', + ], + ]); + + $shippingRate = ShippingRate::factory()->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ + ['price' => 1000, 'min_quantity' => 1, 'currency_id' => $currency->id], + ['price' => 600, 'min_quantity' => 600, 'currency_id' => $currency->id], + ]); + + $variant = ProductVariant::factory()->create([ + 'weight_value' => 3.0, + 'weight_unit' => 'kg', + ]); + $variant->stock = 100; + + Price::factory()->create([ + 'price' => 500, + 'min_quantity' => 1, + 'currency_id' => $currency->id, + 'priceable_type' => $variant->getMorphClass(), + 'priceable_id' => $variant->id, + ]); + + $cart = Cart::factory()->create(['currency_id' => $currency->id]); + $cart->lines()->create([ + 'purchasable_type' => $variant->getMorphClass(), + 'purchasable_id' => $variant->id, + 'quantity' => 2, + ]); + $cart = $cart->calculate(); + + $option = (new ShipBy)->resolve(new ShippingOptionRequest( + shippingRate: $shippingRate, + cart: $cart, + )); + + expect($option)->toBeInstanceOf(ShippingOption::class) + ->and($option->price->value)->toEqual(600); +}); + +test('weight_unit configuration persists in shipping method data', function () { + $shippingMethod = ShippingMethod::factory()->create([ + 'driver' => 'ship-by', + 'data' => [ + 'charge_by' => 'weight', + 'weight_unit' => 'lbs', + ], + ]); + + $shippingMethod->refresh(); + + expect($shippingMethod->data['weight_unit'])->toEqual('lbs') + ->and($shippingMethod->data['charge_by'])->toEqual('weight'); +}); + +test('weight-based shipping converts product units to shipping method weight_unit with x100 scale', function () { + $currency = Currency::factory()->create(['default' => true]); + TaxClass::factory()->create(['default' => true]); + + $shippingZone = ShippingZone::factory()->create(['type' => 'countries']); + + $shippingMethod = ShippingMethod::factory()->create([ + 'driver' => 'ship-by', + 'data' => [ + 'charge_by' => 'weight', + 'weight_unit' => 'kg', + ], + ]); + + $shippingRate = ShippingRate::factory()->create([ + 'shipping_method_id' => $shippingMethod->id, + 'shipping_zone_id' => $shippingZone->id, + ]); + + $shippingRate->prices()->createMany([ + ['price' => 1000, 'min_quantity' => 1, 'currency_id' => $currency->id], + ['price' => 500, 'min_quantity' => 700, 'currency_id' => $currency->id], + ]); + + $variantKg = ProductVariant::factory()->create(['weight_value' => 2.0, 'weight_unit' => 'kg']); + $variantKg->stock = 100; + Price::factory()->create([ + 'price' => 500, 'min_quantity' => 1, 'currency_id' => $currency->id, + 'priceable_type' => $variantKg->getMorphClass(), 'priceable_id' => $variantKg->id, + ]); + + $variantG = ProductVariant::factory()->create(['weight_value' => 5000.0, 'weight_unit' => 'g']); + $variantG->stock = 100; + Price::factory()->create([ + 'price' => 500, 'min_quantity' => 1, 'currency_id' => $currency->id, + 'priceable_type' => $variantG->getMorphClass(), 'priceable_id' => $variantG->id, + ]); + + $cart = Cart::factory()->create(['currency_id' => $currency->id]); + $cart->lines()->createMany([ + ['purchasable_type' => $variantKg->getMorphClass(), 'purchasable_id' => $variantKg->id, 'quantity' => 1], + ['purchasable_type' => $variantG->getMorphClass(), 'purchasable_id' => $variantG->id, 'quantity' => 1], + ]); + $cart = $cart->calculate(); + + $option = (new ShipBy)->resolve(new ShippingOptionRequest( + shippingRate: $shippingRate, + cart: $cart, + )); + + expect($option)->toBeInstanceOf(ShippingOption::class) + ->and($option->price->value)->toEqual(500); +}); \ No newline at end of file From 1e7435a75ee4b9bb4b81e890d1d55ccfcb157e9b Mon Sep 17 00:00:00 2001 From: Author Date: Sat, 13 Jun 2026 08:00:12 +0000 Subject: [PATCH 2/2] chore: fix code style --- .../Unit/Drivers/ShippingMethods/ShipByWeightUnitTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/shipping/Unit/Drivers/ShippingMethods/ShipByWeightUnitTest.php b/tests/shipping/Unit/Drivers/ShippingMethods/ShipByWeightUnitTest.php index 0c11966988..f4fc1e6f68 100644 --- a/tests/shipping/Unit/Drivers/ShippingMethods/ShipByWeightUnitTest.php +++ b/tests/shipping/Unit/Drivers/ShippingMethods/ShipByWeightUnitTest.php @@ -196,4 +196,4 @@ expect($option)->toBeInstanceOf(ShippingOption::class) ->and($option->price->value)->toEqual(500); -}); \ No newline at end of file +});