From 6a6f570efeb0461b91a0be1d6cd13d33b868228d Mon Sep 17 00:00:00 2001 From: Edie Lemoine Date: Wed, 10 Jul 2024 11:13:01 +0200 Subject: [PATCH] fix: make it so shipping methods and classes can both be used fix: fix the shipping things refactor: remove log statements test: rework to single test maintaining same values test: improve coverage style: add comment and change layout wip: increase test coverage refactor: implement more codacy suggestions refactor: implement codacy suggestions chore: update phpstan baseline style: clean upp code test: add mock wc shipping zones and methods test: remove unnecessary class test: remove unnecessary factory test: remove snapshot test: finalize wc context service test test: implement mock wp cache wip: update mock cart and retrieve wp_term feat: remove cart from product settings migration wip: continue with mock classes wip: start with mock classes wip wip: migrate old shipping classes wip: use shipping classes in shipping method logic --- config/pdk.php | 11 +- phpstan-baseline.php | 18 +- .../Pdk/ProductSettingsMigration.php | 176 --------------- src/Migration/Pdk/SettingsMigration.php | 57 ++++- src/Pdk/Context/Service/WcContextService.php | 207 ++++++++++++++++++ src/Pdk/Plugin/WcShippingMethodRepository.php | 105 +++++---- src/Pdk/WcPdkBootstrapper.php | 13 +- .../WcShippingRepositoryInterface.php | 20 ++ .../Repository/WcShippingRepository.php | 59 +++++ tests/Mock/MockWcCart.php | 37 ++-- tests/Mock/MockWcClass.php | 47 +++- tests/Mock/MockWcData.php | 6 +- tests/Mock/MockWcProduct.php | 23 ++ tests/Mock/MockWcSession.php | 36 ++- tests/Mock/MockWcShipping.php | 68 ++++++ tests/Mock/MockWcShippingMethod.php | 10 - tests/Mock/MockWcShippingMethodClass.php | 16 ++ .../MockWcShippingMethodFlatRateClass.php | 39 ++++ tests/Mock/MockWcShippingZone.php | 36 +++ tests/Mock/MockWcShippingZonesClass.php | 98 +++++++++ tests/Mock/MockWpCache.php | 58 +++++ tests/Mock/MockWpTerm.php | 24 ++ tests/Pest.php | 24 +- tests/Unit/Hooks/CheckoutScriptHooksTest.php | 1 - .../Migration/Pdk/SettingsMigrationTest.php | 58 +++++ .../Context/Service/WcContextServiceTest.php | 143 ++++++++++++ .../Plugin/WcShippingMethodRepositoryTest.php | 90 ++++++++ ..._fits_in_mailbox_a_bazillion_times__1.json | 68 +++--- ...mple_product_for_mailbox_too_heavy__1.json | 88 ++++---- ...t_simple_product_with_all_settings__1.json | 118 +++++----- ..._simple_product_with_some_settings__1.json | 98 +++++---- ...ngs_with_data_set_variable_product__1.json | 138 ++++++------ ...uct_with_different_parent_settings__1.json | 138 ++++++------ ....0.0_settings_with_data_set_filled__1.json | 4 +- tests/factories/WC_Product_Factory.php | 1 + .../WC_Shipping_Flat_Rate_Factory.php | 39 ++++ .../factories/WC_Shipping_Method_Factory.php | 35 +++ tests/factories/WC_Shipping_Zone_Factory.php | 20 ++ tests/mock_class_map.php | 25 +++ tests/mock_wp_functions.php | 65 ++++++ .../src/blocks/getBlocksCheckoutConfig.ts | 27 +-- .../checkout-core/src/blocks/index.ts | 6 + views/frontend/checkout-core/src/main.ts | 13 +- views/frontend/checkout-core/src/types.ts | 7 + .../src/utils/getShippingRate.ts | 9 + .../frontend/checkout-core/src/utils/index.ts | 3 + .../checkout-core/src/utils/useCartStore.ts | 16 ++ .../endpoints/fetchHighestShippingClass.ts | 24 -- .../src/endpoints/index.ts | 1 - .../checkout-delivery-options/src/main.ts | 11 +- .../src/utils/getHighestShippingClass.ts | 8 + .../src/utils/index.ts | 1 + 52 files changed, 1789 insertions(+), 654 deletions(-) create mode 100644 src/Pdk/Context/Service/WcContextService.php create mode 100644 src/WooCommerce/Contract/WcShippingRepositoryInterface.php create mode 100644 src/WooCommerce/Repository/WcShippingRepository.php create mode 100644 tests/Mock/MockWcShipping.php delete mode 100644 tests/Mock/MockWcShippingMethod.php create mode 100644 tests/Mock/MockWcShippingMethodClass.php create mode 100644 tests/Mock/MockWcShippingMethodFlatRateClass.php create mode 100644 tests/Mock/MockWcShippingZone.php create mode 100644 tests/Mock/MockWcShippingZonesClass.php create mode 100644 tests/Mock/MockWpCache.php create mode 100644 tests/Mock/MockWpTerm.php create mode 100644 tests/Unit/Pdk/Context/Service/WcContextServiceTest.php create mode 100644 tests/Unit/Pdk/Plugin/WcShippingMethodRepositoryTest.php create mode 100644 tests/factories/WC_Shipping_Flat_Rate_Factory.php create mode 100644 tests/factories/WC_Shipping_Method_Factory.php create mode 100644 tests/factories/WC_Shipping_Zone_Factory.php create mode 100644 views/frontend/checkout-core/src/utils/getShippingRate.ts create mode 100644 views/frontend/checkout-core/src/utils/useCartStore.ts delete mode 100644 views/frontend/checkout-delivery-options/src/endpoints/fetchHighestShippingClass.ts delete mode 100644 views/frontend/checkout-delivery-options/src/endpoints/index.ts create mode 100644 views/frontend/checkout-delivery-options/src/utils/getHighestShippingClass.ts create mode 100644 views/frontend/checkout-delivery-options/src/utils/index.ts diff --git a/config/pdk.php b/config/pdk.php index 38e9c3900..93ad866a2 100644 --- a/config/pdk.php +++ b/config/pdk.php @@ -24,6 +24,7 @@ use MyParcelNL\Pdk\Base\Contract\CronServiceInterface; use MyParcelNL\Pdk\Base\Contract\WeightServiceInterface; use MyParcelNL\Pdk\Base\Support\Arr; +use MyParcelNL\Pdk\Context\Contract\ContextServiceInterface; use MyParcelNL\Pdk\Facade\Language; use MyParcelNL\Pdk\Facade\Pdk; use MyParcelNL\Pdk\Facade\Pdk as PdkFacade; @@ -41,7 +42,9 @@ use MyParcelNL\WooCommerce\Facade\WooCommerce; use MyParcelNL\WooCommerce\Facade\WordPress; use MyParcelNL\WooCommerce\Logger\WcLogger; +use MyParcelNL\WooCommerce\Pdk\Action\Frontend\Context\WcFetchCheckoutContextAction; use MyParcelNL\WooCommerce\Pdk\Audit\Repository\WcPdkAuditRepository; +use MyParcelNL\WooCommerce\Pdk\Context\Service\WcContextService; use MyParcelNL\WooCommerce\Pdk\Guzzle7ClientAdapter; use MyParcelNL\WooCommerce\Pdk\Plugin\Action\WcBackendEndpointService; use MyParcelNL\WooCommerce\Pdk\Plugin\Action\WcFrontendEndpointService; @@ -68,7 +71,9 @@ use MyParcelNL\WooCommerce\Service\WpInstallerService; use MyParcelNL\WooCommerce\Service\WpScriptService; use MyParcelNL\WooCommerce\WooCommerce\Contract\WcOrderRepositoryInterface; +use MyParcelNL\WooCommerce\WooCommerce\Contract\WcShippingRepositoryInterface; use MyParcelNL\WooCommerce\WooCommerce\Repository\WcOrderRepository; +use MyParcelNL\WooCommerce\WooCommerce\Repository\WcShippingRepository; use Psr\Log\LoggerInterface; use function DI\factory; use function DI\get; @@ -170,8 +175,9 @@ # Custom services ### - WordPressServiceInterface::class => get(WordPressService::class), - WooCommerceServiceInterface::class => get(WooCommerceService::class), + WcShippingRepositoryInterface::class => get(WcShippingRepository::class), + WooCommerceServiceInterface::class => get(WooCommerceService::class), + WordPressServiceInterface::class => get(WordPressService::class), ### # PDK services @@ -196,6 +202,7 @@ */ ApiServiceInterface::class => get(MyParcelApiService::class), + ContextServiceInterface::class => get(WcContextService::class), CronServiceInterface::class => get(WpCronService::class), WpDatabaseServiceInterface::class => get(WpDatabaseService::class), InstallerServiceInterface::class => get(WpInstallerService::class), diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 358ab0a5b..c11e8760a 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -2,74 +2,82 @@ $ignoreErrors = []; $ignoreErrors[] = [ + // identifier: ternary.alwaysTrue 'message' => '#^Ternary operator condition is always true\\.$#', 'count' => 1, 'path' => __DIR__ . '/src/Hooks/CartFeesHooks.php', ]; $ignoreErrors[] = [ + // identifier: booleanNot.alwaysFalse 'message' => '#^Negated boolean expression is always false\\.$#', 'count' => 1, 'path' => __DIR__ . '/src/Hooks/CheckoutScriptHooks.php', ]; $ignoreErrors[] = [ + // identifier: arguments.count 'message' => '#^Callback expects 1 parameter, \\$accepted_args is set to 2\\.$#', 'count' => 2, 'path' => __DIR__ . '/src/Hooks/SeparateAddressFieldsHooks.php', ]; $ignoreErrors[] = [ + // identifier: arguments.count 'message' => '#^Callback expects 1 parameter, \\$accepted_args is set to 2\\.$#', 'count' => 2, 'path' => __DIR__ . '/src/Hooks/TaxFieldsHooks.php', ]; $ignoreErrors[] = [ + // identifier: nullCoalesce.property 'message' => '#^Property MyParcelNL\\\\WooCommerce\\\\Migration\\\\Migration4_0_0\\:\\:\\$newCheckoutSettings \\(array\\) on left side of \\?\\? is not nullable\\.$#', 'count' => 1, 'path' => __DIR__ . '/src/Migration/Migration4_0_0.php', ]; $ignoreErrors[] = [ + // identifier: property.onlyWritten 'message' => '#^Property MyParcelNL\\\\WooCommerce\\\\Migration\\\\Migration4_0_0\\:\\:\\$newDpdSettings is never read, only written\\.$#', 'count' => 1, 'path' => __DIR__ . '/src/Migration/Migration4_0_0.php', ]; $ignoreErrors[] = [ + // identifier: nullCoalesce.property 'message' => '#^Property MyParcelNL\\\\WooCommerce\\\\Migration\\\\Migration4_0_0\\:\\:\\$newExportDefaultsSettings \\(array\\) on left side of \\?\\? is not nullable\\.$#', 'count' => 1, 'path' => __DIR__ . '/src/Migration/Migration4_0_0.php', ]; $ignoreErrors[] = [ + // identifier: nullCoalesce.property 'message' => '#^Property MyParcelNL\\\\WooCommerce\\\\Migration\\\\Migration4_0_0\\:\\:\\$newGeneralSettings \\(array\\) on left side of \\?\\? is not nullable\\.$#', 'count' => 1, 'path' => __DIR__ . '/src/Migration/Migration4_0_0.php', ]; $ignoreErrors[] = [ + // identifier: nullCoalesce.property 'message' => '#^Property MyParcelNL\\\\WooCommerce\\\\Migration\\\\Migration4_0_0\\:\\:\\$newPostnlSettings \\(array\\) on left side of \\?\\? is not nullable\\.$#', 'count' => 1, 'path' => __DIR__ . '/src/Migration/Migration4_0_0.php', ]; $ignoreErrors[] = [ + // identifier: arguments.count 'message' => '#^Method MyParcelNL\\\\Pdk\\\\Base\\\\Contract\\\\WeightServiceInterface\\:\\:convertToGrams\\(\\) invoked with 1 parameter, 2 required\\.$#', 'count' => 1, 'path' => __DIR__ . '/src/Migration/Pdk/SettingsMigration.php', ]; $ignoreErrors[] = [ + // identifier: nullCoalesce.variable 'message' => '#^Variable \\$deliveryOptionsData on left side of \\?\\? is never defined\\.$#', 'count' => 1, 'path' => __DIR__ . '/src/Pdk/Hooks/PdkCheckoutPlaceOrderHooks.php', ]; $ignoreErrors[] = [ + // identifier: empty.property 'message' => '#^Property WooCommerce\\:\\:\\$cart \\(WC_Cart\\) in empty\\(\\) is not falsy\\.$#', 'count' => 1, 'path' => __DIR__ . '/src/Pdk/Hooks/PdkFrontendEndpointHooks.php', ]; $ignoreErrors[] = [ + // identifier: assign.propertyType 'message' => '#^Property MyParcelNL\\\\Pdk\\\\App\\\\Order\\\\Model\\\\PdkOrder\\:\\:\\$shipments \\(MyParcelNL\\\\Pdk\\\\Shipment\\\\Collection\\\\ShipmentCollection\\|null\\) does not accept MyParcelNL\\\\Pdk\\\\Base\\\\Support\\\\Collection\\.$#', 'count' => 1, 'path' => __DIR__ . '/src/Pdk/Plugin/Repository/PdkOrderRepository.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Access to an undefined property WooCommerce\\:\\:\\$shipping\\.$#', - 'count' => 1, - 'path' => __DIR__ . '/src/Pdk/Plugin/WcShippingMethodRepository.php', -]; return ['parameters' => ['ignoreErrors' => $ignoreErrors]]; diff --git a/src/Migration/Pdk/ProductSettingsMigration.php b/src/Migration/Pdk/ProductSettingsMigration.php index 48d4cdc6a..3522c0b30 100644 --- a/src/Migration/Pdk/ProductSettingsMigration.php +++ b/src/Migration/Pdk/ProductSettingsMigration.php @@ -13,7 +13,6 @@ use MyParcelNL\Pdk\Settings\Model\CustomsSettings; use MyParcelNL\Pdk\Settings\Model\ProductSettings; use MyParcelNL\Pdk\Shipment\Model\DeliveryOptions; -use MyParcelNL\Sdk\src\Support\Str; use WC_Data; use WC_Meta_Data; use WC_Product; @@ -26,7 +25,6 @@ final class ProductSettingsMigration extends AbstractPdkMigration DeliveryOptions::PACKAGE_TYPE_LETTER_NAME, ]; private const CHUNK_SIZE = 100; - private const FIT_IN_PACKAGE_AMOUNT_LIMIT = 1000; private const LEGACY_META_KEY_HS_CODE = '_myparcel_hs_code'; private const LEGACY_META_KEY_COUNTRY = '_myparcel_country_of_origin'; private const LEGACY_META_KEY_AGE_CHECK = '_myparcel_age_check'; @@ -136,89 +134,6 @@ public function up(): void } } - /** - * @param array $cartShippingMethods - * @param \WC_Product $wcProduct - * - * @return array - */ - private function addFlatRateShippingClass(array $cartShippingMethods, WC_Product $wcProduct): array - { - $shippingClassId = $wcProduct->get_shipping_class_id(); - - if ($shippingClassId) { - foreach ($cartShippingMethods as $cartShippingMethod) { - if (! Str::startsWith($cartShippingMethod, 'flat_rate:')) { - continue; - } - - $cartShippingMethods[] = "flat_rate:$shippingClassId"; - - break; - } - } - - return $cartShippingMethods; - } - - /** - * @param string $packageType - * @param \WC_Product $wcProduct - * @param int $amountInPackageType - * @param int $increment - * @param int $previousIncrement - * - * @return int - * @throws \Exception - */ - private function calculateAmountFitInPackageType( - string $packageType, - WC_Product $wcProduct, - int $amountInPackageType = 0, - int $increment = 1, - int $previousIncrement = 0 - ): int { - if ($amountInPackageType > self::FIT_IN_PACKAGE_AMOUNT_LIMIT) { - return self::FIT_IN_PACKAGE_AMOUNT_LIMIT; - } - - WC()->cart->add_to_cart($wcProduct->get_id(), $increment); - - $fitsInPackageType = array_intersect( - $this->getShippingMethodsForPackageType($packageType), - $this->getCartShippingMethods($wcProduct) - ); - - if (! $fitsInPackageType && $previousIncrement > 1) { - WC()->cart->empty_cart(); - WC()->cart->add_to_cart($wcProduct->get_id(), $amountInPackageType - $previousIncrement); - - return $this->calculateAmountFitInPackageType( - $packageType, - $wcProduct, - $amountInPackageType, - 1, - $increment - ); - } - - if ($fitsInPackageType) { - $amountInPackageType += $increment; - - return $this->calculateAmountFitInPackageType( - $packageType, - $wcProduct, - $amountInPackageType, - $increment * 2, - $increment - ); - } - - WC()->cart->empty_cart(); - - return $amountInPackageType; - } - /** * @return array */ @@ -235,36 +150,6 @@ private function getAllProductIds(): array }, $allProducts); } - /** - * @param \WC_Product $product - * - * @return array - */ - private function getCartShippingMethods(WC_Product $product): array - { - $cartShippingPackages = WC()->cart->get_shipping_packages(); - - return $this->addFlatRateShippingClass($this->getMethodsFromPackages($cartShippingPackages), $product); - } - - /** - * @param array $cartShippingMethods - * - * @return string - */ - private function getMatchingPackageType(array $cartShippingMethods): string - { - foreach (self::PACKAGE_TYPES as $packageType) { - if (! array_intersect($this->getShippingMethodsForPackageType($packageType), $cartShippingMethods)) { - continue; - } - - return $packageType; - } - - return 'package'; - } - /** * @param \WC_Data $data * @@ -279,65 +164,6 @@ private function getMetaData(WC_Data $data): Collection ); } - /** - * @param array $cartShippingPackages - * - * @return array - */ - private function getMethodsFromPackages(array $cartShippingPackages): array - { - $cartShippingMethods = []; - - foreach (array_keys($cartShippingPackages) as $key) { - $shippingForPackage = WC()->session->get("shipping_for_package_$key"); - - $cartShippingMethods += array_keys($shippingForPackage['rates'] ?? []); - } - - return $cartShippingMethods; - } - - /** - * @throws \Exception - */ - private function getSettingsForPackageType(WC_Product $wcProduct): array - { - if (null === WC()->cart) { - wc_load_cart(); - } - - WC()->cart->empty_cart(); - WC()->cart->add_to_cart($wcProduct->get_id()); - - return [ - ProductSettings::FIT_IN_MAILBOX => $this->calculateAmountFitInPackageType( - DeliveryOptions::PACKAGE_TYPE_MAILBOX_NAME, - $wcProduct - ), - ProductSettings::FIT_IN_DIGITAL_STAMP => $this->calculateAmountFitInPackageType( - DeliveryOptions::PACKAGE_TYPE_DIGITAL_STAMP_NAME, - $wcProduct - ), - ProductSettings::PACKAGE_TYPE => $this->getMatchingPackageType( - $this->getCartShippingMethods($wcProduct) - ), - ]; - } - - /** - * @param string $packageType - * - * @return array - */ - private function getShippingMethodsForPackageType(string $packageType): array - { - $legacySettings = get_option(SettingsMigration::LEGACY_OPTION_EXPORT_DEFAULTS_SETTINGS) ?: []; - - $shippingMethods = $legacySettings['shipping_methods_package_types'] ?? []; - - return $shippingMethods[$packageType] ?? []; - } - /** * @param \WC_Product $wcProduct * @@ -359,8 +185,6 @@ private function migrateProduct(WC_Product $wcProduct): void $pdkProduct->settings->setAttribute($newKey, $this->normalizeValue($metaData['value'])); } - $pdkProduct->settings->fill($this->getSettingsForPackageType($wcProduct)); - $this->pdkProductRepository->update($pdkProduct); $migrationMeta = $this->getMigrationMeta($wcProduct); diff --git a/src/Migration/Pdk/SettingsMigration.php b/src/Migration/Pdk/SettingsMigration.php index 6bbe8dbfd..d2974a4c8 100644 --- a/src/Migration/Pdk/SettingsMigration.php +++ b/src/Migration/Pdk/SettingsMigration.php @@ -8,11 +8,15 @@ use MyParcelNL\Pdk\Base\Contract\CurrencyServiceInterface; use MyParcelNL\Pdk\Base\Contract\WeightServiceInterface; use MyParcelNL\Pdk\Base\Support\Arr; +use MyParcelNL\Pdk\Base\Support\Collection; use MyParcelNL\Pdk\Facade\Pdk; use MyParcelNL\Pdk\Settings\Collection\SettingsModelCollection; use MyParcelNL\Pdk\Settings\Contract\PdkSettingsRepositoryInterface; use MyParcelNL\Pdk\Settings\Model\Settings; use MyParcelNL\Pdk\Shipment\Model\DropOffDay; +use MyParcelNL\Sdk\src\Support\Str; +use MyParcelNL\WooCommerce\WooCommerce\Repository\WcShippingRepository; +use WP_Term; class SettingsMigration extends AbstractPdkMigration { @@ -99,7 +103,6 @@ public function migrateSettings(array $oldSettings): void } $settings = new Settings($newSettings); - $settingsRepository->storeAllSettings($settings); } @@ -175,6 +178,48 @@ private function castValue(string $cast, $value) } } + /** + * @param string $shippingMethod + * @param \MyParcelNL\Pdk\Base\Support\Collection $wcShippingMethods + * + * @return string + */ + private function convertShippingMethodId(string $shippingMethod, Collection $wcShippingMethods): string + { + if (! Str::contains($shippingMethod, ':')) { + return $shippingMethod; + } + + [$shippingMethodName, $termId] = explode(':', $shippingMethod); + + if ($termId) { + $match = $wcShippingMethods + ->filter(function ($shippingMethod) use ($termId, $shippingMethodName) { + return $shippingMethod->id === $shippingMethodName && (int) $shippingMethod->instance_id === (int) $termId; + }) + ->first(); + + if (! $match) { + /** @var WP_Term|null $foundShippingClass */ + $foundShippingClass = $wcShippingMethods + ->filter(function ($shippingMethod) use ($termId) { + if (! $shippingMethod instanceof WP_Term) { + return false; + } + + return $shippingMethod->term_id === (int) $termId; + }) + ->first(); + + if ($foundShippingClass) { + return Pdk::get('createShippingClassName')($foundShippingClass->term_id); + } + } + } + + return $shippingMethod; + } + private function getOldSettings(): array { return [ @@ -383,6 +428,11 @@ private function getTransformationMap(): Generator return $newValue; } + $wcShippingRepository = Pdk::get(WcShippingRepository::class); + $wcShippingMethods = $wcShippingRepository + ->getShippingMethods() + ->merge($wcShippingRepository->getShippingClasses()); + $allShippingMethods = array_unique(Arr::flatten($value)); // Find the smallest enabled package type for each shipping method, and add it to the new map. @@ -392,7 +442,10 @@ private function getTransformationMap(): Generator })); $smallestPackageType = Arr::last($packageTypes); - $newValue[$smallestPackageType][] = $shippingMethod; + $newValue[$smallestPackageType][] = $this->convertShippingMethodId( + $shippingMethod, + $wcShippingMethods + ); } return $newValue; diff --git a/src/Pdk/Context/Service/WcContextService.php b/src/Pdk/Context/Service/WcContextService.php new file mode 100644 index 000000000..be1afac44 --- /dev/null +++ b/src/Pdk/Context/Service/WcContextService.php @@ -0,0 +1,207 @@ +getHighestShippingClass($cart, $checkoutContext->settings['allowedShippingMethods']); + + $checkoutContext->settings = array_merge($checkoutContext->settings, [ + 'highestShippingClass' => $highestShippingClass ?? '', + ]); + + return $checkoutContext; + } + + /** + * @param array $allowedShippingMethods + * + * @return null|string + */ + private function getCartShippingClass(array $allowedShippingMethods): ?string + { + $cart = WC()->cart->get_cart(); + + $highest = null; + + foreach ($cart as $cartItem) { + $data = $cartItem['data']; + $shippingClassTermId = $data->get_shipping_class(); + if (! $shippingClassTermId) { + continue; + } + + $createShippingClassName = Pdk::get('createShippingClassName'); + $shippingClassName = + $createShippingClassName($this->getShippingClassId($shippingClassTermId)); + $shippingClassHasDeliveryOptions = + $this->shippingMethodOrClassHasDeliveryOptions($shippingClassName, $allowedShippingMethods); + + if ($shippingClassHasDeliveryOptions) { + $highest = $shippingClassTermId; + } + } + + return $highest; + } + + /** + * Checks if a shipping class or method is enabled by it being set in allowed shipping methods. + * + * @param string $methodOrClassName + * @param array $allowedShippingMethods + * + * @return bool + */ + private function shippingMethodOrClassHasDeliveryOptions( + string $methodOrClassName, + array $allowedShippingMethods + ): bool { + foreach ($allowedShippingMethods as $packageType) { + if (in_array($methodOrClassName, $packageType, false)) { + return true; + } + } + + return false; + } + + /** + * @param null|string $method + * + * @return null|\WC_Shipping_Method + */ + private function getCurrentShippingMethod(?string $method): ?WC_Shipping_Method + { + $methodString = $method ?? WC()->session->get('chosen_shipping_methods')[0]; + + if (! $methodString) { + return null; + } + + $parts = explode(':', $methodString); + $instanceId = $parts[1] ?? null; + + /** @var \WC_Shipping_Method|null $shippingMethod */ + $shippingMethod = WC_Shipping_Zones::get_shipping_method((int) $instanceId) ?: null; + + return $shippingMethod; + } + + /** + * @param null|\MyParcelNL\Pdk\App\Cart\Model\PdkCart $cart + * @param array $allowedShippingMethods + * + * @return null|string + */ + private function getHighestShippingClass(?PdkCart $cart, ?array $allowedShippingMethods): ?string + { + $shippingMethod = $this->getCurrentShippingMethod($cart->shippingMethod->id ?? null); + $cartShippingClass = null; + if ($allowedShippingMethods) { + $cartShippingClass = $this->getCartShippingClass($allowedShippingMethods); + } + + if (! $cartShippingClass || ! $shippingMethod) { + return null; + } + + $shippingClasses = $this->getShippingClasses($shippingMethod); + + foreach ($shippingClasses as $shippingClass) { + if (! $this->hasShippingClassCost($shippingClass, $shippingMethod)) { + continue; + } + + $createShippingClassName = Pdk::get('createShippingClassName'); + + return $createShippingClassName($this->getShippingClassId($shippingClass)); + } + + return null; + } + + /** + * @param string $shippingClass + * + * @return null|int + */ + private function getShippingClassId(string $shippingClass): ?int + { + $term = get_term_by('slug', $shippingClass, 'product_shipping_class'); + + return $this->getTermId($term); + } + + /** + * @param \WC_Shipping_Method $shippingMethod + * + * @return array + */ + private function getShippingClasses(WC_Shipping_Method $shippingMethod): array + { + if (! method_exists($shippingMethod, 'find_shipping_classes')) { + return []; + } + + $packages = WC()->cart->get_shipping_packages(); + $package = current($packages); + + $shippingClasses = $shippingMethod->find_shipping_classes($package); + + return array_filter(array_keys($shippingClasses)); + } + + /** + * @param WP_Term|array|null $term + * + * @return null|int + */ + private function getTermId($term): ?int + { + $termId = null; + + if ($term instanceof WP_Term) { + $termId = $term->term_id; + } elseif (is_array($term)) { + $termId = $term['term_id'] ?? null; + } + + return $termId ? (int) $termId : null; + } + + /** + * @param string $shippingClass + * @param \WC_Shipping_Method $shippingMethod + * + * @return bool + */ + private function hasShippingClassCost(string $shippingClass, WC_Shipping_Method $shippingMethod): bool + { + $id = $this->getShippingClassId($shippingClass); + + $classCost = $shippingMethod->get_option("class_cost_$shippingClass"); + $termCost = $shippingMethod->get_option("class_cost_$id"); + + return (bool) ($termCost ?: $classCost); + } +} diff --git a/src/Pdk/Plugin/WcShippingMethodRepository.php b/src/Pdk/Plugin/WcShippingMethodRepository.php index 1c33e447d..83f777388 100644 --- a/src/Pdk/Plugin/WcShippingMethodRepository.php +++ b/src/Pdk/Plugin/WcShippingMethodRepository.php @@ -6,65 +6,83 @@ use InvalidArgumentException; use MyParcelNL\Pdk\App\ShippingMethod\Collection\PdkShippingMethodCollection; +use MyParcelNL\Pdk\App\ShippingMethod\Contract\PdkShippingMethodRepositoryInterface; use MyParcelNL\Pdk\App\ShippingMethod\Model\PdkShippingMethod; -use MyParcelNL\Pdk\App\ShippingMethod\Repository\AbstractPdkShippingMethodRepository; -use WC_Shipping; +use MyParcelNL\Pdk\Facade\Pdk; +use MyParcelNL\WooCommerce\WooCommerce\Contract\WcShippingRepositoryInterface; +use WC_Shipping_Local_Pickup; use WC_Shipping_Method; -use WC_Shipping_Zones; +use WP_Term; -class WcShippingMethodRepository extends AbstractPdkShippingMethodRepository +class WcShippingMethodRepository implements PdkShippingMethodRepositoryInterface { + /** + * @var \MyParcelNL\WooCommerce\WooCommerce\Contract\WcShippingRepositoryInterface + */ + private $wcShippingRepository; + + public function __construct(WcShippingRepositoryInterface $wcShippingRepository) + { + $this->wcShippingRepository = $wcShippingRepository; + } + /** * Get all available shipping methods from WooCommerce. + * + * @return \MyParcelNL\Pdk\App\ShippingMethod\Collection\PdkShippingMethodCollection */ public function all(): PdkShippingMethodCollection { // The "0" zone is the "Rest of the World" zone in WooCommerce. - $zoneIds = array_merge([0], array_keys(WC_Shipping_Zones::get_zones())); - - return new PdkShippingMethodCollection( - array_reduce( - $zoneIds, - function (array $carry, $zoneId): array { - $zoneInstance = WC_Shipping_Zones::get_zone($zoneId); - - foreach ($zoneInstance->get_shipping_methods() as $shippingMethod) { - $carry[] = $this->get($shippingMethod); - } - - return $carry; - }, - [] - ) - ); + $wcShippingMethods = $this->wcShippingRepository->getShippingMethods(); + $wcShippingClasses = $this->wcShippingRepository->getShippingClasses(); + + $createdShippingMethods = $wcShippingMethods + ->merge($wcShippingClasses) + ->map(function ($method) { + if ($method instanceof WC_Shipping_Method) { + return $this->createFromWcShippingMethod($method); + } + + if ($method instanceof WP_Term) { + return $this->createFromWcShippingClass($method); + } + + throw new InvalidArgumentException('Unknown shipping method type'); + }); + + return new PdkShippingMethodCollection($createdShippingMethods); } /** - * @param \WC_Shipping_Method|PdkShippingMethod|string $input + * @param \WP_Term $shippingClass * * @return \MyParcelNL\Pdk\App\ShippingMethod\Model\PdkShippingMethod */ - public function get($input): PdkShippingMethod + public function createFromWcShippingClass(WP_Term $shippingClass): PdkShippingMethod { - if ($input instanceof PdkShippingMethod) { - return $input; - } - - if ($input instanceof WC_Shipping_Method) { - $method = $input; - } else { - $wcShipping = $this->wcShipping(); - $method = $wcShipping->get_shipping_methods()[$input] ?? null; - } + $id = Pdk::get('createShippingClassName')($shippingClass->term_id); - if (! $method) { - throw new InvalidArgumentException('Shipping method not found'); - } + return new PdkShippingMethod([ + 'id' => $id, + 'name' => "📦️ $shippingClass->name (Shipping class)", + 'description' => "ID: $id", + 'isEnabled' => true, + ]); + } + /** + * @param \WC_Shipping_Method $method + * + * @return \MyParcelNL\Pdk\App\ShippingMethod\Model\PdkShippingMethod + */ + private function createFromWcShippingMethod(WC_Shipping_Method $method): PdkShippingMethod + { return new PdkShippingMethod([ - 'id' => "$method->id:$method->instance_id", - 'name' => $this->getShippingMethodTitle($method), - 'isEnabled' => $method->enabled === 'yes', + 'id' => $method->get_rate_id(), + 'name' => $this->getShippingMethodTitle($method), + 'description' => "ID: {$method->get_rate_id()}", + 'isEnabled' => 'yes' === $method->enabled && ! $method instanceof WC_Shipping_Local_Pickup, ]); } @@ -82,13 +100,4 @@ private function getShippingMethodTitle(WC_Shipping_Method $method): string return $methodTitle . $suffix; } - - /** - * @return \WC_Shipping - * @noinspection PhpUndefinedFieldInspection - */ - private function wcShipping(): WC_Shipping - { - return WC()->shipping; - } } diff --git a/src/Pdk/WcPdkBootstrapper.php b/src/Pdk/WcPdkBootstrapper.php index 8cd4d18e9..4770c40bb 100644 --- a/src/Pdk/WcPdkBootstrapper.php +++ b/src/Pdk/WcPdkBootstrapper.php @@ -324,8 +324,17 @@ protected function getAdditionalConfig( # WP Cron actions - 'webhookAddActions' => value("{$name}_all_actions"), - 'webhookActionName' => value("{$name}_hook_"), + 'webhookAddActions' => value("{$name}_all_actions"), + 'webhookActionName' => value("{$name}_hook_"), + + /** + * Generate a name for a shipping class by its term id + */ + 'createShippingClassName' => factory(function () { + return static function (int $id): string { + return "shipping_class:$id"; + }; + }), ]); } } diff --git a/src/WooCommerce/Contract/WcShippingRepositoryInterface.php b/src/WooCommerce/Contract/WcShippingRepositoryInterface.php new file mode 100644 index 000000000..1f643068f --- /dev/null +++ b/src/WooCommerce/Contract/WcShippingRepositoryInterface.php @@ -0,0 +1,20 @@ + + */ + public function getShippingMethods(): Collection; +} diff --git a/src/WooCommerce/Repository/WcShippingRepository.php b/src/WooCommerce/Repository/WcShippingRepository.php new file mode 100644 index 000000000..92fac949a --- /dev/null +++ b/src/WooCommerce/Repository/WcShippingRepository.php @@ -0,0 +1,59 @@ +retrieve(__METHOD__, function () { + /** @var WC_Shipping $wcShipping */ + $wcShipping = WC_Shipping::instance(); + $classes = $wcShipping->get_shipping_classes(); + + return new Collection($classes); + }); + } + + /** + * @return \MyParcelNL\Pdk\Base\Support\Collection<\WC_Shipping_Method> + */ + public function getShippingMethods(): Collection + { + return $this->retrieve(__METHOD__, function () { + // The "0" zone is the "Rest of the World" zone in WooCommerce. + $zoneIds = array_merge([0], array_keys(WC_Shipping_Zones::get_zones())); + $shippingMethods = []; + + foreach ($zoneIds as $zoneId) { + $zoneInstance = WC_Shipping_Zones::get_zone($zoneId); + + /** @var WC_Shipping_Method[] $shippingMethods */ + $zoneShippingMethods = $zoneInstance->get_shipping_methods(true); + + foreach ($zoneShippingMethods as $shippingMethod) { + $shippingMethods[] = $shippingMethod; + } + } + + return new Collection($shippingMethods); + }); + } + + protected function getKeyPrefix(): string + { + return static::class; + } +} diff --git a/tests/Mock/MockWcCart.php b/tests/Mock/MockWcCart.php index cefa53902..8d9cbe9ab 100644 --- a/tests/Mock/MockWcCart.php +++ b/tests/Mock/MockWcCart.php @@ -14,7 +14,7 @@ class MockWcCart extends MockWcClass /** * @var \WC_Product[] */ - private $items = []; + public $cart_contents = []; /** * @param int $productId @@ -38,9 +38,11 @@ public function add_to_cart( $cartItemKey = $this->find_product_in_cart($cartId); if ($cartItemKey) { - $this->items[$cartItemKey]['quantity'] += $quantity; + $this->cart_contents[$cartItemKey]['quantity'] += $quantity; } else { - $this->items[] = [ + $cartItemKey = $cartId; + + $this->cart_contents[$cartItemKey] = [ 'data' => new WC_Product($productId), 'quantity' => $quantity, ]; @@ -49,7 +51,7 @@ public function add_to_cart( public function get_cart() { - return $this->items; + return $this->cart_contents; } /** @@ -57,7 +59,7 @@ public function get_cart() */ public function empty_cart(): void { - $this->items = []; + $this->cart_contents = []; } /** @@ -65,21 +67,14 @@ public function empty_cart(): void */ public function get_shipping_packages(): array { - // calculate weight of all products in cart - $weight = 0; - foreach ($this->items as $item) { - /** @var \WC_Product $wcProduct */ - $wcProduct = $item['data']; - $weight += $wcProduct->get_weight() * $item['quantity']; - } - - if ($weight > 10) { - return []; - } - - return [ - 'flat_rate:0' => [], + $shippingPackages = [ + ['contents' => $this->cart_contents], ]; + + return apply_filters( + 'woocommerce_cart_shipping_packages', + $shippingPackages + ); } /** @@ -140,8 +135,8 @@ public function generate_cart_id( */ public function find_product_in_cart($cartId = false): string { - $thisItemsIsArray = is_array($this->items); - $itemAlreadyExists = isset($this->items[$cartId]); + $thisItemsIsArray = is_array($this->cart_contents); + $itemAlreadyExists = isset($this->cart_contents[$cartId]); if ($cartId !== false && $thisItemsIsArray && $itemAlreadyExists) { return $cartId; diff --git a/tests/Mock/MockWcClass.php b/tests/Mock/MockWcClass.php index 4b874caca..9f51a8926 100644 --- a/tests/Mock/MockWcClass.php +++ b/tests/Mock/MockWcClass.php @@ -33,13 +33,11 @@ public function __construct($data = []) } /** - * @return null|int + * @return null|int|string */ - public function get_id(): ?int + public function get_id() { - $id = $this->attributes['id'] ?? null; - - return is_numeric($id) ? (int) $id : null; + return $this->attributes['id'] ?? null; } /** @@ -76,7 +74,7 @@ public function save(): void /** * @return void */ - public function save_meta_data() + public function save_meta_data(): void { // do nothing } @@ -118,4 +116,41 @@ protected function fill(array $data): void update_post_meta($created->get_id(), $metaKey, $metaValue); } } + + /** + * Dynamically retrieve attributes on the model. + * + * @param string $key + * + * @return mixed + */ + public function __get(string $key) + { + return $this->attributes[$key] ?? null; + } + + /** + * Dynamically set attributes on the model. + * + * @param string $key + * @param mixed $value + * + * @return void + */ + public function __set(string $key, $value): void + { + $this->attributes[$key] = $value; + } + + /** + * Determine if an attribute or relation exists on the model. + * + * @param string $key + * + * @return bool + */ + public function __isset(string $key) + { + return isset($this->attributes[$key]); + } } diff --git a/tests/Mock/MockWcData.php b/tests/Mock/MockWcData.php index 76d77f040..5dc5a69a8 100644 --- a/tests/Mock/MockWcData.php +++ b/tests/Mock/MockWcData.php @@ -22,7 +22,7 @@ final class MockWcData implements StaticMockInterface */ public static function create(MockWcClass $data): MockWcClass { - if (! $data->get_id()) { + if (! $data->get_id() && ! $data->get_instance_id()) { $data->set_id(count(self::$items) + 1); } @@ -78,13 +78,13 @@ public static function reset(): void */ public static function save(MockWcClass $data): MockWcClass { - $id = (string) $data->get_id(); + $id = $data->get_instance_id() ?? $data->get_id(); if (! $id) { throw new RuntimeException('Cannot save data without id'); } - self::$items[$id] = $data; + self::$items[(string) $id] = $data; return $data; } diff --git a/tests/Mock/MockWcProduct.php b/tests/Mock/MockWcProduct.php index 4922eea8e..0c86f8072 100644 --- a/tests/Mock/MockWcProduct.php +++ b/tests/Mock/MockWcProduct.php @@ -5,6 +5,8 @@ namespace MyParcelNL\WooCommerce\Tests\Mock; +use WP_Term; + /** * @extends \WC_Product */ @@ -23,4 +25,25 @@ public function get_shipping_class_id(): int { return $this->attributes['shipping_class_id']; } + + /** + * Returns the product shipping class SLUG. + * + * @return string + */ + public function get_shipping_class(): string + { + $classId = $this->get_shipping_class_id(); + $slug = ''; + if ($classId) { + $term = get_term_by('id', $classId, 'product_shipping_class'); + if ($term instanceof WP_Term) { + $slug = $term->slug; + } elseif (is_array($term)) { + $slug = $term['slug'] ?? null; + } + } + + return $slug; + } } diff --git a/tests/Mock/MockWcSession.php b/tests/Mock/MockWcSession.php index 91aed110e..b97279a2f 100644 --- a/tests/Mock/MockWcSession.php +++ b/tests/Mock/MockWcSession.php @@ -4,13 +4,17 @@ namespace MyParcelNL\WooCommerce\Tests\Mock; -use BadMethodCallException; -use MyParcelNL\Pdk\Base\Support\Arr; -use MyParcelNL\Sdk\src\Support\Str; -use WC_Data; - -class MockWcSession extends MockWcClass +/** + * @extends \WC_Session + */ +class MockWcSession implements StaticMockInterface { + private static $session = [ + 'rates' => [ + 'flat_rate:0' => [], + ], + ]; + /** * @param $key * @@ -18,10 +22,26 @@ class MockWcSession extends MockWcClass */ public function get($key): array { - return [ + return self::$session[$key] ?? [ 'rates' => [ 'flat_rate:0' => [], - ] + ], ]; } + + /** + * @param $key + * @param $value + * + * @return void + */ + public function set($key, $value): void + { + self::$session[$key] = $value; + } + + public static function reset(): void + { + self::$session = []; + } } diff --git a/tests/Mock/MockWcShipping.php b/tests/Mock/MockWcShipping.php new file mode 100644 index 000000000..751ce2f1c --- /dev/null +++ b/tests/Mock/MockWcShipping.php @@ -0,0 +1,68 @@ +shipping_classes; + } +} \ No newline at end of file diff --git a/tests/Mock/MockWcShippingMethod.php b/tests/Mock/MockWcShippingMethod.php deleted file mode 100644 index f8c4cfa8d..000000000 --- a/tests/Mock/MockWcShippingMethod.php +++ /dev/null @@ -1,10 +0,0 @@ -attributes['instance_settings'][$key]; + } +} diff --git a/tests/Mock/MockWcShippingMethodFlatRateClass.php b/tests/Mock/MockWcShippingMethodFlatRateClass.php new file mode 100644 index 000000000..3d3017a58 --- /dev/null +++ b/tests/Mock/MockWcShippingMethodFlatRateClass.php @@ -0,0 +1,39 @@ + $values) { + if ($values['data']->needs_shipping()) { + $foundClass = $values['data']->get_shipping_class(); + + if (! isset($foundShippingClasses[$foundClass])) { + $foundShippingClasses[$foundClass] = []; + } + + $foundShippingClasses[$foundClass][$itemId] = $values; + } + } + + return $foundShippingClasses; + } +} diff --git a/tests/Mock/MockWcShippingZone.php b/tests/Mock/MockWcShippingZone.php new file mode 100644 index 000000000..3083804d8 --- /dev/null +++ b/tests/Mock/MockWcShippingZone.php @@ -0,0 +1,36 @@ +get_supports()['settings']['shipping_zone_id'] === $this->get_id()) { + if ($enabledOnly && $method->get_enabled === 'no') { + continue; + } + $filteredMethods[] = $method; + } + } + + return $filteredMethods; + } +} diff --git a/tests/Mock/MockWcShippingZonesClass.php b/tests/Mock/MockWcShippingZonesClass.php new file mode 100644 index 000000000..1085c7f3d --- /dev/null +++ b/tests/Mock/MockWcShippingZonesClass.php @@ -0,0 +1,98 @@ +get_id()] = $zone; + } + + return $allZonesArray; + } + + /** + * Get shipping zone using its ID + * + * @param int $zoneId Zone ID. + * + * @return WC_Shipping_Zone|bool + * @throws \Throwable + * @since 2.6.0 + */ + public static function get_zone(int $zoneId) + { + return self::get_zone_by('zone_id', $zoneId); + } + + /** + * Get shipping zone by an ID. + * + * @param string $by Get by 'zone_id' or 'instance_id'. + * @param int $id ID. + * + * @return WC_Shipping_Zone|bool + * @throws \Throwable + * @since 2.6.0 + */ + public static function get_zone_by($by = 'zone_id', $id = 0) + { + $zoneId = false; + + switch ($by) { + case 'zone_id': + $zoneId = $id; + break; + } + + if (false !== $zoneId) { + try { + return new WC_Shipping_Zone($zoneId); + } catch (Exception $e) { + return false; + } + } + + return false; + } +} diff --git a/tests/Mock/MockWpCache.php b/tests/Mock/MockWpCache.php new file mode 100644 index 000000000..55da24d6e --- /dev/null +++ b/tests/Mock/MockWpCache.php @@ -0,0 +1,58 @@ +afterEach(function () { - MockWcPdkBootstrapper::reset(); - - MockWpActions::reset(); - MockWcData::reset(); - MockWpMeta::reset(); + /** + * @var $resetInterfaces class-string<\MyParcelNL\WooCommerce\Tests\Mock\StaticMockInterface>[] + */ + $resetInterfaces = [ + MockWcData::class, + MockWcPdkBootstrapper::class, + MockWcSession::class, + MockWpActions::class, + MockWpEnqueue::class, + MockWpMeta::class, + MockWpCache::class, + ]; + + foreach ($resetInterfaces as $class) { + $class::reset(); + } }) ->in(__DIR__); diff --git a/tests/Unit/Hooks/CheckoutScriptHooksTest.php b/tests/Unit/Hooks/CheckoutScriptHooksTest.php index 4d1703b3f..d95c05230 100644 --- a/tests/Unit/Hooks/CheckoutScriptHooksTest.php +++ b/tests/Unit/Hooks/CheckoutScriptHooksTest.php @@ -48,7 +48,6 @@ function ( ->toHaveKeys($expected['toContain']) ->and($all)->not->toHaveKeys($expected['notToContain']); - MockWpEnqueue::reset(); WC()->cart->empty_cart(); } ) diff --git a/tests/Unit/Migration/Pdk/SettingsMigrationTest.php b/tests/Unit/Migration/Pdk/SettingsMigrationTest.php index 1e2f99022..abd046f82 100644 --- a/tests/Unit/Migration/Pdk/SettingsMigrationTest.php +++ b/tests/Unit/Migration/Pdk/SettingsMigrationTest.php @@ -8,7 +8,12 @@ use MyParcelNL\Pdk\Facade\Pdk; use MyParcelNL\Pdk\Facade\Settings; use MyParcelNL\WooCommerce\Tests\Uses\UsesMockWcPdkInstance; +use WC_Shipping; +use WC_Shipping_Method; +use WC_Shipping_Zone; +use WP_Term; use function MyParcelNL\Pdk\Tests\usesShared; +use function MyParcelNL\WooCommerce\Tests\wpFactory; use function Spatie\Snapshots\assertMatchesJsonSnapshot; usesShared(new UsesMockWcPdkInstance()); @@ -155,6 +160,59 @@ ]); it('migrates pre v5.0.0 settings', function (array $settings) { + wpFactory(WC_Shipping_Zone::class) + ->withId(1) + ->withData([ + 'zone_name' => 'Nederland', + 'zone_order' => 1, + 'zone_locations' => [], + ]) + ->store(); + + wpFactory(WC_Shipping_Method::class) + ->withSupports([ + 'settings' => [ + 'shipping_zone_id' => 1, + ], + ]) + ->withId('flat_rate') + ->withInstanceId('32') + ->withMethodTitle('Flat rate Nederland') + ->withEnabled('yes') + ->store(); + + wpFactory(WC_Shipping_Zone::class) + ->withId(2) + ->withData([ + 'zone_name' => 'België', + 'zone_order' => 1, + 'zone_locations' => [], + ]) + ->store(); + + wpFactory(WC_Shipping_Method::class) + ->withSupports([ + 'settings' => [ + 'shipping_zone_id' => 2, + ], + ]) + ->withId('flat_rate') + ->withInstanceId('33') + ->withMethodTitle('Flat rate België') + ->withEnabled('yes') + ->store(); + + $wpTerm = new WP_Term(); + $wpTerm->term_id = 5; + $wpTerm->name = 'table rate'; + $wpTerm->slug = 'table-rate'; + + wp_cache_add((string) $wpTerm->term_id, $wpTerm, 'terms'); + + $wcShipping = WC_Shipping::instance(); + $wcShipping->enabled = true; + $wcShipping->shipping_classes = [$wpTerm]; + /** @var \MyParcelNL\WooCommerce\Migration\Pdk\SettingsMigration $migration */ $migration = Pdk::get(SettingsMigration::class); $migration->migrateSettings($settings); diff --git a/tests/Unit/Pdk/Context/Service/WcContextServiceTest.php b/tests/Unit/Pdk/Context/Service/WcContextServiceTest.php new file mode 100644 index 000000000..5d4634183 --- /dev/null +++ b/tests/Unit/Pdk/Context/Service/WcContextServiceTest.php @@ -0,0 +1,143 @@ +term_id, [ + 'term_id' => $wpTerm->term_id, + 'slug' => $wpTerm->slug, + 'name' => $wpTerm->name, + ], 'terms'); + + return; + } + wp_cache_add((string) $wpTerm->term_id, $wpTerm, 'terms'); +} + +function add_product_to_cart(?int $shippingClassId = null) +{ + $product_id = 6789; + $wcProduct = wpFactory(WC_Product::class) + ->withId($product_id); + if (null !== $shippingClassId) { + $wcProduct->withShippingClassId($shippingClassId); + } + $wcProduct->make(); + + WC()->cart->add_to_cart($product_id); +} + +it('creates checkout context', function ($input, $expected) { + $contextService = Pdk::get(WcContextService::class); + $cartRepository = Pdk::get(PdkCartRepositoryInterface::class); + + $shippingClass = $input['shippingMethod']; + $shippingClassId = $input['shippingClassId'] ?? null; + $shippingPrice = $input['shippingPrice'] ?? 0; + $termAsArray = $input['termAsArray'] ?? false; + + if ($shippingClassId) { + $wpTerm = new WP_Term(); + $wpTerm->term_id = 12; + $wpTerm->name = 'shipping class package'; + $wpTerm->slug = 'shipping-class-package'; + + add_product_to_cart($wpTerm->term_id); + + add_term_to_cache($wpTerm, $termAsArray); + } else { + add_product_to_cart(); + } + + $pdkCart = $cartRepository->get(WC()->cart); + + $pdkShippingMethod = factory(PdkShippingMethod::class) + ->withId('flexible_shipping:456') + ->make(); + $pdkCart->shippingMethod = $pdkShippingMethod; + + $shippingMethod = wpFactory($shippingClass) + ->withId(456); + if ($shippingPrice) { + $shippingMethod->instance_settings["class_cost_$wpTerm->term_id"] = $shippingPrice; + } + $shippingMethod->store(); + + $checkoutContext = $contextService->createCheckoutContext($pdkCart); + + expect($checkoutContext->config->basePrice) + ->toBe($expected['basePrice']) + ->and($checkoutContext->settings['highestShippingClass']) + ->toBe($expected['highestShippingClass']); +})->with([ + 'flexible shipping' => [ + 'input' => [ + 'shippingMethod' => WC_Shipping_Method::class, + 'shippingClassId' => 12, + ], + 'expected' => [ + 'basePrice' => 0.0, + 'highestShippingClass' => '', + ], + ], + 'flat rate with price' => [ + 'input' => [ + 'shippingMethod' => WC_Shipping_Flat_Rate::class, + 'shippingClassId' => 12, + 'shippingPrice' => 5.12, + ], + 'expected' => [ + 'basePrice' => 0.0, + 'highestShippingClass' => '', + ], + ], + 'product without shipping class' => [ + 'input' => [ + 'shippingMethod' => WC_Shipping_Flat_Rate::class, + ], + 'expected' => [ + 'basePrice' => 0.0, + 'highestShippingClass' => '', + ], + ], + 'flat rate without price' => [ + 'input' => [ + 'shippingMethod' => WC_Shipping_Flat_Rate::class, + 'shippingClassId' => 12, + ], + 'expected' => [ + 'basePrice' => 0.0, + 'highestShippingClass' => '', + ], + ], + 'term as array' => [ + 'input' => [ + 'shippingMethod' => WC_Shipping_Flat_Rate::class, + 'shippingClassId' => 12, + 'termAsArray' => true, + ], + 'expected' => [ + 'basePrice' => 0.0, + 'highestShippingClass' => '', + ], + ], +]); diff --git a/tests/Unit/Pdk/Plugin/WcShippingMethodRepositoryTest.php b/tests/Unit/Pdk/Plugin/WcShippingMethodRepositoryTest.php new file mode 100644 index 000000000..8807f48bf --- /dev/null +++ b/tests/Unit/Pdk/Plugin/WcShippingMethodRepositoryTest.php @@ -0,0 +1,90 @@ +enabled = true; + $wcShipping->shipping_classes = []; + + foreach ($input as $method) { + // convert array to wp_term + if (is_array($method)) { + $wpTerm = new WP_Term(); + $wpTerm->term_id = (int) $method['id']; + $wpTerm->name = str_replace('-', ' ', $method['slug']); + $wpTerm->slug = $method['slug']; + wp_cache_add((string) $wpTerm->term_id, $wpTerm, 'terms'); + $wcShipping->shipping_classes[] = $wpTerm; + continue; + } + $wcShipping->shipping_classes[] = $method; + } + + $wcShippingRepository = Pdk::get(WcShippingRepository::class); + $wcShippingMethodRepository = new WcShippingMethodRepository($wcShippingRepository); + $shippingMethods = array_map(static function ($item) { + return ['id' => $item['id'], 'name' => $item['name']]; + }, + $wcShippingMethodRepository->all() + ->toArray()); + + expect(count($shippingMethods))->toBe(count($input)); +})->with([ + 'shipping methods' => [ + 'input' => [ + new \WC_Shipping_Method(['id' => 1, 'method_title' => 'table-rate']), + new \WC_Shipping_Method(['id' => 5, 'method_title' => 'flatrate']), + ], + ], + 'wp terms and methods' => [ + 'input' => [ + ['id' => 1, 'name' => 'table-rate'], + new \WC_Shipping_Method(['id' => 127, 'method_title' => 'flexible-shipping']), + ], + ], +]); + +it('throws error for illegal input', function ($input) { + $wcShipping = WC_Shipping::instance(); + $wcShipping->enabled = true; + $wcShipping->shipping_classes = []; + + foreach ($input as $method) { + $wcShipping->shipping_classes[] = $method; + } + + $wcShippingRepository = Pdk::get(WcShippingRepository::class); + $wcShippingMethodRepository = new WcShippingMethodRepository($wcShippingRepository); + + expect($wcShippingMethodRepository->all())->toBeInstanceOf(PdkShippingMethodCollection::class); +}) + ->with([ + 'string' => [ + [ + 'string', + new \WC_Shipping_Method(['id' => 127, 'method_title' => 'flexible-shipping']), + ], + ], + 'null' => [ + [ + null, + new \WC_Shipping_Method(['id' => 127, 'method_title' => 'flexible-shipping']), + ], + ], + ]) + ->throws(\InvalidArgumentException::class); + diff --git a/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_product_that_fits_in_mailbox_a_bazillion_times__1.json b/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_product_that_fits_in_mailbox_a_bazillion_times__1.json index 74a064d1e..9554fc439 100644 --- a/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_product_that_fits_in_mailbox_a_bazillion_times__1.json +++ b/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_product_that_fits_in_mailbox_a_bazillion_times__1.json @@ -1,9 +1,35 @@ { - "meta": [ - { - "id": 0, - "key": "_myparcelnl_product_settings", - "value": { + "meta": [ + { + "id": 0, + "key": "_myparcelnl_product_settings", + "value": { + "countryOfOrigin": -1, + "customsCode": -1, + "disableDeliveryOptions": -1, + "dropOffDelay": -1, + "exportAgeCheck": -1, + "exportHideSender": -1, + "exportInsurance": -1, + "exportLargeFormat": -1, + "exportOnlyRecipient": -1, + "exportReturn": -1, + "exportSignature": -1, + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 + } + }, + { + "id": 0, + "key": "_myparcelnl_migrated", + "value": [ + "5.0.0-alpha.1" + ] + } + ], + "product": { + "id": "product", "countryOfOrigin": -1, "customsCode": -1, "disableDeliveryOptions": -1, @@ -15,33 +41,9 @@ "exportOnlyRecipient": -1, "exportReturn": -1, "exportSignature": -1, - "fitInDigitalStamp": 1000, - "fitInMailbox": 1000, - "packageType": "digital_stamp" - } + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 }, - { - "id": 0, - "key": "_myparcelnl_migrated", - "value": ["5.0.0-alpha.1"] - } - ], - "product": { - "id": "product", - "countryOfOrigin": -1, - "customsCode": -1, - "disableDeliveryOptions": -1, - "dropOffDelay": -1, - "exportAgeCheck": -1, - "exportHideSender": -1, - "exportInsurance": -1, - "exportLargeFormat": -1, - "exportOnlyRecipient": -1, - "exportReturn": -1, - "exportSignature": -1, - "fitInDigitalStamp": 1000, - "fitInMailbox": 1000, - "packageType": "digital_stamp" - }, - "parentProduct": null + "parentProduct": null } diff --git a/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_simple_product_for_mailbox_too_heavy__1.json b/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_simple_product_for_mailbox_too_heavy__1.json index a9fe9f191..7cecb2b69 100644 --- a/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_simple_product_for_mailbox_too_heavy__1.json +++ b/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_simple_product_for_mailbox_too_heavy__1.json @@ -1,19 +1,45 @@ { - "meta": [ - { - "id": 0, - "key": "_virtual", - "value": "no" - }, - { - "id": 0, - "key": "_weight", - "value": 2.1 - }, - { - "id": 0, - "key": "_myparcelnl_product_settings", - "value": { + "meta": [ + { + "id": 0, + "key": "_virtual", + "value": "no" + }, + { + "id": 0, + "key": "_weight", + "value": 2.1 + }, + { + "id": 0, + "key": "_myparcelnl_product_settings", + "value": { + "countryOfOrigin": -1, + "customsCode": -1, + "disableDeliveryOptions": -1, + "dropOffDelay": -1, + "exportAgeCheck": -1, + "exportHideSender": -1, + "exportInsurance": -1, + "exportLargeFormat": -1, + "exportOnlyRecipient": -1, + "exportReturn": -1, + "exportSignature": -1, + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 + } + }, + { + "id": 0, + "key": "_myparcelnl_migrated", + "value": [ + "5.0.0-alpha.1" + ] + } + ], + "product": { + "id": "product", "countryOfOrigin": -1, "customsCode": -1, "disableDeliveryOptions": -1, @@ -25,33 +51,9 @@ "exportOnlyRecipient": -1, "exportReturn": -1, "exportSignature": -1, - "fitInDigitalStamp": 1000, - "fitInMailbox": 1000, - "packageType": "digital_stamp" - } + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 }, - { - "id": 0, - "key": "_myparcelnl_migrated", - "value": ["5.0.0-alpha.1"] - } - ], - "product": { - "id": "product", - "countryOfOrigin": -1, - "customsCode": -1, - "disableDeliveryOptions": -1, - "dropOffDelay": -1, - "exportAgeCheck": -1, - "exportHideSender": -1, - "exportInsurance": -1, - "exportLargeFormat": -1, - "exportOnlyRecipient": -1, - "exportReturn": -1, - "exportSignature": -1, - "fitInDigitalStamp": 1000, - "fitInMailbox": 1000, - "packageType": "digital_stamp" - }, - "parentProduct": null + "parentProduct": null } diff --git a/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_simple_product_with_all_settings__1.json b/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_simple_product_with_all_settings__1.json index f551bf49a..bd83651c4 100644 --- a/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_simple_product_with_all_settings__1.json +++ b/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_simple_product_with_all_settings__1.json @@ -1,34 +1,60 @@ { - "meta": [ - { - "id": 0, - "key": "_myparcel_age_check", - "value": "no" - }, - { - "id": 0, - "key": "_myparcel_country_of_origin", - "value": "BB" - }, - { - "id": 0, - "key": "_myparcel_hs_code", - "value": "1234" - }, - { - "id": 0, - "key": "_virtual", - "value": "no" - }, - { - "id": 0, - "key": "_weight", - "value": 0.3 - }, - { - "id": 0, - "key": "_myparcelnl_product_settings", - "value": { + "meta": [ + { + "id": 0, + "key": "_myparcel_age_check", + "value": "no" + }, + { + "id": 0, + "key": "_myparcel_country_of_origin", + "value": "BB" + }, + { + "id": 0, + "key": "_myparcel_hs_code", + "value": "1234" + }, + { + "id": 0, + "key": "_virtual", + "value": "no" + }, + { + "id": 0, + "key": "_weight", + "value": 0.3 + }, + { + "id": 0, + "key": "_myparcelnl_product_settings", + "value": { + "countryOfOrigin": "BB", + "customsCode": "1234", + "disableDeliveryOptions": -1, + "dropOffDelay": -1, + "exportAgeCheck": 0, + "exportHideSender": -1, + "exportInsurance": -1, + "exportLargeFormat": -1, + "exportOnlyRecipient": -1, + "exportReturn": -1, + "exportSignature": -1, + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 + } + }, + { + "id": 0, + "key": "_myparcelnl_migrated", + "value": [ + "5.0.0-alpha.1" + ] + } + ], + "product": { + "id": "product", "countryOfOrigin": "BB", "customsCode": "1234", "disableDeliveryOptions": -1, @@ -40,33 +66,9 @@ "exportOnlyRecipient": -1, "exportReturn": -1, "exportSignature": -1, - "fitInDigitalStamp": 7, - "fitInMailbox": 7, - "packageType": "digital_stamp" - } + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 }, - { - "id": 0, - "key": "_myparcelnl_migrated", - "value": ["5.0.0-alpha.1"] - } - ], - "parentProduct": null, - "product": { - "countryOfOrigin": "BB", - "customsCode": "1234", - "disableDeliveryOptions": -1, - "dropOffDelay": -1, - "exportAgeCheck": 0, - "exportHideSender": -1, - "exportInsurance": -1, - "exportLargeFormat": -1, - "exportOnlyRecipient": -1, - "exportReturn": -1, - "exportSignature": -1, - "fitInDigitalStamp": 7, - "fitInMailbox": 7, - "id": "product", - "packageType": "digital_stamp" - } + "parentProduct": null } diff --git a/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_simple_product_with_some_settings__1.json b/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_simple_product_with_some_settings__1.json index 9f2dbdb89..987c8aa6c 100644 --- a/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_simple_product_with_some_settings__1.json +++ b/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_simple_product_with_some_settings__1.json @@ -1,24 +1,50 @@ { - "meta": [ - { - "id": 0, - "key": "_myparcel_age_check", - "value": "yes" - }, - { - "id": 0, - "key": "_virtual", - "value": "no" - }, - { - "id": 0, - "key": "_weight", - "value": 0.3 - }, - { - "id": 0, - "key": "_myparcelnl_product_settings", - "value": { + "meta": [ + { + "id": 0, + "key": "_myparcel_age_check", + "value": "yes" + }, + { + "id": 0, + "key": "_virtual", + "value": "no" + }, + { + "id": 0, + "key": "_weight", + "value": 0.3 + }, + { + "id": 0, + "key": "_myparcelnl_product_settings", + "value": { + "countryOfOrigin": -1, + "customsCode": -1, + "disableDeliveryOptions": -1, + "dropOffDelay": -1, + "exportAgeCheck": 1, + "exportHideSender": -1, + "exportInsurance": -1, + "exportLargeFormat": -1, + "exportOnlyRecipient": -1, + "exportReturn": -1, + "exportSignature": -1, + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 + } + }, + { + "id": 0, + "key": "_myparcelnl_migrated", + "value": [ + "5.0.0-alpha.1" + ] + } + ], + "product": { + "id": "product", "countryOfOrigin": -1, "customsCode": -1, "disableDeliveryOptions": -1, @@ -30,33 +56,9 @@ "exportOnlyRecipient": -1, "exportReturn": -1, "exportSignature": -1, - "fitInDigitalStamp": 1000, - "fitInMailbox": 1000, - "packageType": "digital_stamp" - } + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 }, - { - "id": 0, - "key": "_myparcelnl_migrated", - "value": ["5.0.0-alpha.1"] - } - ], - "product": { - "id": "product", - "countryOfOrigin": -1, - "customsCode": -1, - "disableDeliveryOptions": -1, - "dropOffDelay": -1, - "exportAgeCheck": 1, - "exportHideSender": -1, - "exportInsurance": -1, - "exportLargeFormat": -1, - "exportOnlyRecipient": -1, - "exportReturn": -1, - "exportSignature": -1, - "fitInDigitalStamp": 1000, - "fitInMailbox": 1000, - "packageType": "digital_stamp" - }, - "parentProduct": null + "parentProduct": null } diff --git a/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_variable_product__1.json b/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_variable_product__1.json index 597bfd8f1..063a1a93f 100644 --- a/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_variable_product__1.json +++ b/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_variable_product__1.json @@ -1,29 +1,55 @@ { - "meta": [ - { - "id": 0, - "key": "_myparcel_country_of_origin_variation", - "value": "AD" - }, - { - "id": 0, - "key": "_myparcel_hs_code_variation", - "value": "9090" - }, - { - "id": 0, - "key": "_virtual", - "value": "no" - }, - { - "id": 0, - "key": "_weight", - "value": 0.3 - }, - { - "id": 0, - "key": "_myparcelnl_product_settings", - "value": { + "meta": [ + { + "id": 0, + "key": "_myparcel_country_of_origin_variation", + "value": "AD" + }, + { + "id": 0, + "key": "_myparcel_hs_code_variation", + "value": "9090" + }, + { + "id": 0, + "key": "_virtual", + "value": "no" + }, + { + "id": 0, + "key": "_weight", + "value": 0.3 + }, + { + "id": 0, + "key": "_myparcelnl_product_settings", + "value": { + "countryOfOrigin": "AD", + "customsCode": "9090", + "disableDeliveryOptions": -1, + "dropOffDelay": -1, + "exportAgeCheck": -1, + "exportHideSender": -1, + "exportInsurance": -1, + "exportLargeFormat": -1, + "exportOnlyRecipient": -1, + "exportReturn": -1, + "exportSignature": -1, + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 + } + }, + { + "id": 0, + "key": "_myparcelnl_migrated", + "value": [ + "5.0.0-alpha.1" + ] + } + ], + "product": { + "id": "product", "countryOfOrigin": "AD", "customsCode": "9090", "disableDeliveryOptions": -1, @@ -35,49 +61,25 @@ "exportOnlyRecipient": -1, "exportReturn": -1, "exportSignature": -1, - "fitInDigitalStamp": 1000, - "fitInMailbox": 1000, - "packageType": "digital_stamp" - } + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 }, - { - "id": 0, - "key": "_myparcelnl_migrated", - "value": ["5.0.0-alpha.1"] + "parentProduct": { + "id": "product", + "countryOfOrigin": -1, + "customsCode": -1, + "disableDeliveryOptions": -1, + "dropOffDelay": -1, + "exportAgeCheck": -1, + "exportHideSender": -1, + "exportInsurance": -1, + "exportLargeFormat": -1, + "exportOnlyRecipient": -1, + "exportReturn": -1, + "exportSignature": -1, + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 } - ], - "product": { - "id": "product", - "countryOfOrigin": "AD", - "customsCode": "9090", - "disableDeliveryOptions": -1, - "dropOffDelay": -1, - "exportAgeCheck": -1, - "exportHideSender": -1, - "exportInsurance": -1, - "exportLargeFormat": -1, - "exportOnlyRecipient": -1, - "exportReturn": -1, - "exportSignature": -1, - "fitInDigitalStamp": 1000, - "fitInMailbox": 1000, - "packageType": "digital_stamp" - }, - "parentProduct": { - "id": "product", - "countryOfOrigin": -1, - "customsCode": -1, - "disableDeliveryOptions": -1, - "dropOffDelay": -1, - "exportAgeCheck": -1, - "exportHideSender": -1, - "exportInsurance": -1, - "exportLargeFormat": -1, - "exportOnlyRecipient": -1, - "exportReturn": -1, - "exportSignature": -1, - "fitInDigitalStamp": -1, - "fitInMailbox": -1, - "packageType": -1 - } } diff --git a/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_variable_product_with_different_parent_settings__1.json b/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_variable_product_with_different_parent_settings__1.json index 6b56cd2d3..e316c8f39 100644 --- a/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_variable_product_with_different_parent_settings__1.json +++ b/tests/__snapshots__/ProductSettingsMigrationTest__it_migrates_pre_v5.0.0_product_settings_with_data_set_variable_product_with_different_parent_settings__1.json @@ -1,29 +1,55 @@ { - "meta": [ - { - "id": 0, - "key": "_myparcel_country_of_origin_variation", - "value": "AD" - }, - { - "id": 0, - "key": "_myparcel_hs_code_variation", - "value": "5432" - }, - { - "id": 0, - "key": "_virtual", - "value": "no" - }, - { - "id": 0, - "key": "_weight", - "value": 0.3 - }, - { - "id": 0, - "key": "_myparcelnl_product_settings", - "value": { + "meta": [ + { + "id": 0, + "key": "_myparcel_country_of_origin_variation", + "value": "AD" + }, + { + "id": 0, + "key": "_myparcel_hs_code_variation", + "value": "5432" + }, + { + "id": 0, + "key": "_virtual", + "value": "no" + }, + { + "id": 0, + "key": "_weight", + "value": 0.3 + }, + { + "id": 0, + "key": "_myparcelnl_product_settings", + "value": { + "countryOfOrigin": "AD", + "customsCode": "5432", + "disableDeliveryOptions": -1, + "dropOffDelay": -1, + "exportAgeCheck": -1, + "exportHideSender": -1, + "exportInsurance": -1, + "exportLargeFormat": -1, + "exportOnlyRecipient": -1, + "exportReturn": -1, + "exportSignature": -1, + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 + } + }, + { + "id": 0, + "key": "_myparcelnl_migrated", + "value": [ + "5.0.0-alpha.1" + ] + } + ], + "product": { + "id": "product", "countryOfOrigin": "AD", "customsCode": "5432", "disableDeliveryOptions": -1, @@ -35,49 +61,25 @@ "exportOnlyRecipient": -1, "exportReturn": -1, "exportSignature": -1, - "fitInDigitalStamp": 1000, - "fitInMailbox": 1000, - "packageType": "digital_stamp" - } + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 }, - { - "id": 0, - "key": "_myparcelnl_migrated", - "value": ["5.0.0-alpha.1"] + "parentProduct": { + "id": "product", + "countryOfOrigin": -1, + "customsCode": -1, + "disableDeliveryOptions": -1, + "dropOffDelay": -1, + "exportAgeCheck": -1, + "exportHideSender": -1, + "exportInsurance": -1, + "exportLargeFormat": -1, + "exportOnlyRecipient": -1, + "exportReturn": -1, + "exportSignature": -1, + "fitInDigitalStamp": -1, + "fitInMailbox": -1, + "packageType": -1 } - ], - "product": { - "id": "product", - "countryOfOrigin": "AD", - "customsCode": "5432", - "disableDeliveryOptions": -1, - "dropOffDelay": -1, - "exportAgeCheck": -1, - "exportHideSender": -1, - "exportInsurance": -1, - "exportLargeFormat": -1, - "exportOnlyRecipient": -1, - "exportReturn": -1, - "exportSignature": -1, - "fitInDigitalStamp": 1000, - "fitInMailbox": 1000, - "packageType": "digital_stamp" - }, - "parentProduct": { - "id": "product", - "countryOfOrigin": -1, - "customsCode": -1, - "disableDeliveryOptions": -1, - "dropOffDelay": -1, - "exportAgeCheck": -1, - "exportHideSender": -1, - "exportInsurance": -1, - "exportLargeFormat": -1, - "exportOnlyRecipient": -1, - "exportReturn": -1, - "exportSignature": -1, - "fitInDigitalStamp": -1, - "fitInMailbox": -1, - "packageType": -1 - } } diff --git a/tests/__snapshots__/SettingsMigrationTest__it_migrates_pre_v5.0.0_settings_with_data_set_filled__1.json b/tests/__snapshots__/SettingsMigrationTest__it_migrates_pre_v5.0.0_settings_with_data_set_filled__1.json index 6f3395896..f6394b13f 100644 --- a/tests/__snapshots__/SettingsMigrationTest__it_migrates_pre_v5.0.0_settings_with_data_set_filled__1.json +++ b/tests/__snapshots__/SettingsMigrationTest__it_migrates_pre_v5.0.0_settings_with_data_set_filled__1.json @@ -47,10 +47,10 @@ "flat_rate:32", "free_shipping", "local_pickup", - "table_rate:5:2" + "shipping_class:5" ], "package_small": [], - "mailbox": ["table_rate:5:1"], + "mailbox": ["shipping_class:5"], "digital_stamp": ["flat_rate:30", "flat_rate:31"], "letter": ["flat_rate:33"] }, diff --git a/tests/factories/WC_Product_Factory.php b/tests/factories/WC_Product_Factory.php index a90c1fdfa..93626a8a8 100644 --- a/tests/factories/WC_Product_Factory.php +++ b/tests/factories/WC_Product_Factory.php @@ -19,6 +19,7 @@ * @method $this withWeight(float $weight) * @method $this withWidth(float $width) * @method $this withSettings(array $settings) + * @method $this withShippingClassId(int $id) */ final class WC_Product_Factory extends AbstractWcDataFactory { diff --git a/tests/factories/WC_Shipping_Flat_Rate_Factory.php b/tests/factories/WC_Shipping_Flat_Rate_Factory.php new file mode 100644 index 000000000..869551f02 --- /dev/null +++ b/tests/factories/WC_Shipping_Flat_Rate_Factory.php @@ -0,0 +1,39 @@ +slug === $value) { + return $term; + } + + if (is_array($term) && $term['slug'] === $value) { + return $term; + } + } + } + + return false; +} + +/** + * @param int|\Wp_Term $term + * @param $taxonomy + * @param $output + * @param $filter + * + * @return false|mixed|\WP_Term + */ +function get_term($term, $taxonomy = '', $output = OBJECT, $filter = 'raw') +{ + if ($term instanceof WP_Term) { + return MockWpTerm::get_instance($term->term_id, $taxonomy); + } + + return MockWpTerm::get_instance($term, $taxonomy); +} + +function wp_cache_add(string $key, $data, string $group = '', int $expire = 0): bool +{ + return MockWpCache::add($key, $data, $group, $expire); +} + +/** + * @param int|string $key + * @param string $group + * @param bool $force + * @param $found + * + * @return false|mixed + */ +function wp_cache_get($key, string $group = '', bool $force = false, &$found = null) +{ + return MockWpCache::get($key, $group, $force, $found); +} + + /** * @return \WP_REST_Server|MockWpRestServer * @see \rest_get_server() diff --git a/views/frontend/checkout-core/src/blocks/getBlocksCheckoutConfig.ts b/views/frontend/checkout-core/src/blocks/getBlocksCheckoutConfig.ts index 76f0cef09..ad4d1661e 100644 --- a/views/frontend/checkout-core/src/blocks/getBlocksCheckoutConfig.ts +++ b/views/frontend/checkout-core/src/blocks/getBlocksCheckoutConfig.ts @@ -1,21 +1,10 @@ import {AddressType, PdkField} from '@myparcel-pdk/checkout-common'; -import {PdkUtil, useUtil} from '@myparcel-pdk/checkout'; +import {PdkUtil, useUtil, type PdkFormData, updateContext} from '@myparcel-pdk/checkout'; +import {useCartStore, getShippingRate} from '../utils'; import {type CheckoutConfig} from '../types'; -function useCartStore() { - const {CART_STORE_KEY} = window.wc.wcBlocksData; - - return wp.data.select(CART_STORE_KEY); -} - -const getShippingRate = () => { - const cartStore = useCartStore(); - const shippingRates = cartStore.getShippingRates(); - - return shippingRates[0].shipping_rates.find((rate) => rate.selected); -}; - -export const getBlocksCheckoutConfig = () => { +// eslint-disable-next-line max-lines-per-function +export const getBlocksCheckoutConfig = (): CheckoutConfig => { const addressFields = { address1: `address_1`, address2: `address_2`, @@ -47,11 +36,11 @@ export const getBlocksCheckoutConfig = () => { let previousShippingRate = getShippingRate(); let previousCustomerData = JSON.stringify(cartStore.getCustomerData()); - wp.data.subscribe(() => { + wp.data.subscribe(async () => { const currentShippingRate = getShippingRate(); const currentCustomerData = cartStore.getCustomerData(); - const shippingMethodChanged = previousShippingRate.rate_id !== currentShippingRate.rate_id; + const shippingMethodChanged = previousShippingRate?.rate_id !== currentShippingRate?.rate_id; const customerDataChanged = previousCustomerData !== JSON.stringify(currentCustomerData); if (!shippingMethodChanged && !customerDataChanged) { @@ -64,6 +53,8 @@ export const getBlocksCheckoutConfig = () => { if (shippingMethodChanged) { previousShippingRate = currentShippingRate; + + await updateContext(); } callback(); @@ -73,7 +64,7 @@ export const getBlocksCheckoutConfig = () => { getFormData() { const cartStore = useCartStore(); const customerData = cartStore.getCustomerData(); - const formData = {}; + const formData: PdkFormData = {}; [AddressType.Shipping, AddressType.Billing].forEach((addressType) => { Object.keys(addressFields).forEach((field) => { diff --git a/views/frontend/checkout-core/src/blocks/index.ts b/views/frontend/checkout-core/src/blocks/index.ts index e404993d6..1923079b0 100644 --- a/views/frontend/checkout-core/src/blocks/index.ts +++ b/views/frontend/checkout-core/src/blocks/index.ts @@ -1 +1,7 @@ +export type {WcCartStore} from '../types'; + +export type {WcShippingRate} from '../types'; + +export type {WpStore} from '../types'; + export * from './getBlocksCheckoutConfig'; diff --git a/views/frontend/checkout-core/src/main.ts b/views/frontend/checkout-core/src/main.ts index b08474ed2..6f2deed9e 100644 --- a/views/frontend/checkout-core/src/main.ts +++ b/views/frontend/checkout-core/src/main.ts @@ -1,3 +1,4 @@ +import {getHighestShippingClass} from '@myparcel-woocommerce/frontend-checkout-delivery-options/src/utils/getHighestShippingClass'; import {PdkField, AddressType} from '@myparcel-pdk/checkout-common'; import {createPdkCheckout, getEnabledShippingMethods} from '@myparcel-pdk/checkout'; import {isClassicCheckout, createName, createId, createFields} from './utils'; @@ -45,7 +46,17 @@ createPdkCheckout({ hasDeliveryOptions(shippingMethod) { const shippingMethods = getEnabledShippingMethods(); - return shippingMethods.some((method) => shippingMethod === method || shippingMethod.startsWith(`${method}:`)); + const shippingMethodHasDeliveryOptions = shippingMethods.some((method) => { + return shippingMethod === method || shippingMethod.startsWith(`${method}:`); + }); + + if (shippingMethodHasDeliveryOptions) { + return true; + } + + const shippingClass = getHighestShippingClass(); + + return shippingClass !== undefined && shippingMethods.includes(shippingClass); }, toggleField(field: HTMLInputElement, show: boolean): void { diff --git a/views/frontend/checkout-core/src/types.ts b/views/frontend/checkout-core/src/types.ts index 73d57d9fa..246621cbe 100644 --- a/views/frontend/checkout-core/src/types.ts +++ b/views/frontend/checkout-core/src/types.ts @@ -9,3 +9,10 @@ export interface CheckoutConfig = prefixShipping: string; shippingMethodFormField: string; } + +export interface WpStore {} + +export interface WcShippingRate { + rate_id: string; + selected: boolean; +} diff --git a/views/frontend/checkout-core/src/utils/getShippingRate.ts b/views/frontend/checkout-core/src/utils/getShippingRate.ts new file mode 100644 index 000000000..05229393d --- /dev/null +++ b/views/frontend/checkout-core/src/utils/getShippingRate.ts @@ -0,0 +1,9 @@ +import {type WcShippingRate} from '../types'; +import {useCartStore} from './useCartStore'; + +export const getShippingRate = (): WcShippingRate | undefined => { + const cartStore = useCartStore(); + const shippingRates = cartStore.getShippingRates(); + + return shippingRates[0].shipping_rates.find((rate) => rate.selected); +}; diff --git a/views/frontend/checkout-core/src/utils/index.ts b/views/frontend/checkout-core/src/utils/index.ts index 54cda9a9c..e77b2b04e 100644 --- a/views/frontend/checkout-core/src/utils/index.ts +++ b/views/frontend/checkout-core/src/utils/index.ts @@ -2,3 +2,6 @@ export * from './createFields'; export * from './createId'; export * from './createName'; export * from './isClassicCheckout'; +export {getShippingRate} from './getShippingRate'; + +export {useCartStore} from './useCartStore'; diff --git a/views/frontend/checkout-core/src/utils/useCartStore.ts b/views/frontend/checkout-core/src/utils/useCartStore.ts new file mode 100644 index 000000000..f7409527b --- /dev/null +++ b/views/frontend/checkout-core/src/utils/useCartStore.ts @@ -0,0 +1,16 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import {type WcShippingRate, type WpStore} from '../types'; + +export interface WcCartStore extends WpStore { + getCustomerData(): { + shippingAddress: Record; + }; + + getShippingRates(): [{shipping_rates: WcShippingRate[]}]; +} + +export const useCartStore = (): WcCartStore => { + const {CART_STORE_KEY} = window.wc.wcBlocksData; + + return wp.data.select(CART_STORE_KEY); +}; diff --git a/views/frontend/checkout-delivery-options/src/endpoints/fetchHighestShippingClass.ts b/views/frontend/checkout-delivery-options/src/endpoints/fetchHighestShippingClass.ts deleted file mode 100644 index 5589dd041..000000000 --- a/views/frontend/checkout-delivery-options/src/endpoints/fetchHighestShippingClass.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Get the highest shipping class by doing a call to WordPress. We're getting it this way and not from the - * highest_shipping_class input because that causes some kind of timing issue which makes the delivery options not - * show up. - * - * @returns {String|null} - */ -export const fetchHighestShippingClass = () => { - let shippingClass = null; - - jQuery.ajax({ - type: 'POST', - url: MyParcelNLData.ajaxUrl, - async: false, - data: { - action: 'get_highest_shipping_class', - }, - success(data) { - shippingClass = data; - }, - }); - - return shippingClass; -}; diff --git a/views/frontend/checkout-delivery-options/src/endpoints/index.ts b/views/frontend/checkout-delivery-options/src/endpoints/index.ts deleted file mode 100644 index 114868cd4..000000000 --- a/views/frontend/checkout-delivery-options/src/endpoints/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './fetchHighestShippingClass'; diff --git a/views/frontend/checkout-delivery-options/src/main.ts b/views/frontend/checkout-delivery-options/src/main.ts index 3c3d57d3c..1b142aae0 100644 --- a/views/frontend/checkout-delivery-options/src/main.ts +++ b/views/frontend/checkout-delivery-options/src/main.ts @@ -4,10 +4,19 @@ import { initializeCheckoutDeliveryOptions as initialize, useEvent, usePdkCheckout, + getPackageTypeFromShippingMethod, + defaultGetPackageType, } from '@myparcel-pdk/checkout'; +import {getHighestShippingClass} from './utils'; const initializeCheckoutDeliveryOptions = () => { - void initialize(); + initialize({ + getPackageType() { + const shippingClass = getHighestShippingClass(); + + return shippingClass ? getPackageTypeFromShippingMethod(shippingClass) : defaultGetPackageType(); + }, + }); document.addEventListener(useEvent(PdkDeliveryOptionsEvent.DeliveryOptionsUpdated), () => { jQuery(document.body).trigger('update_checkout'); diff --git a/views/frontend/checkout-delivery-options/src/utils/getHighestShippingClass.ts b/views/frontend/checkout-delivery-options/src/utils/getHighestShippingClass.ts new file mode 100644 index 000000000..b79567caf --- /dev/null +++ b/views/frontend/checkout-delivery-options/src/utils/getHighestShippingClass.ts @@ -0,0 +1,8 @@ +import {useCheckoutStore} from '@myparcel-pdk/checkout'; + +export const getHighestShippingClass = (): undefined | string => { + const checkout = useCheckoutStore(); + + // Empty string is a valid value, so we need to check for undefined + return checkout.state.context.settings.highestShippingClass || undefined; +}; diff --git a/views/frontend/checkout-delivery-options/src/utils/index.ts b/views/frontend/checkout-delivery-options/src/utils/index.ts new file mode 100644 index 000000000..9e4ad8e1d --- /dev/null +++ b/views/frontend/checkout-delivery-options/src/utils/index.ts @@ -0,0 +1 @@ +export * from './getHighestShippingClass';