latteEngine = $latteEngine; $this->carrierRepository = $carrierRepository; $this->formFactory = $formFactory; $this->httpRequest = $httpRequest; $this->countryListingPage = $countryListingPage; $this->messageManager = $messageManager; $this->pickupPointsConfig = $pickupPointsConfig; $this->featureFlag = $featureFlag; } /** * Registers WP callbacks. */ public function register(): void { add_submenu_page( \Packetery\Module\Options\Page::SLUG, __( 'Carrier settings', 'packeta' ), __( 'Carrier settings', 'packeta' ), 'manage_options', self::SLUG, array( $this, 'render', ), 10 ); } /** * Creates settings form. * * @param array $carrierData Carrier data. * * @return Form */ private function createForm( array $carrierData ): Form { $optionId = OptionPrefixer::getOptionId( $carrierData['id'] ); $form = $this->formFactory->create( $optionId ); $form->addCheckbox( 'active', __( 'Active carrier', 'packeta' ) . ':' ); $form->addText( self::FORM_FIELD_NAME, __( 'Display name', 'packeta' ) . ':' ) ->setRequired(); $carrierOptions = get_option( $optionId ); if ( $this->featureFlag->isSplitActive() ) { $vendorCheckboxes = $this->getVendorCheckboxesConfig( $carrierData['id'], ( $carrierOptions ? $carrierOptions : null ) ); if ( $vendorCheckboxes ) { $vendorsContainer = $form->addContainer( 'vendor_groups' ); foreach ( $vendorCheckboxes as $checkboxConfig ) { $checkboxControl = $vendorsContainer->addCheckbox( $checkboxConfig['group'], $checkboxConfig['name'] ); if ( true === $checkboxConfig['disabled'] ) { $checkboxControl->setDisabled()->setOmitted( false ); } if ( true === $checkboxConfig['default'] ) { $checkboxControl->setDefaultValue( true ); } } } } $weightLimits = $form->addContainer( 'weight_limits' ); if ( empty( $carrierData['weight_limits'] ) ) { $this->addWeightLimit( $weightLimits, 0 ); } else { foreach ( $carrierData['weight_limits'] as $index => $limit ) { $this->addWeightLimit( $weightLimits, $index ); } } $carrier = $this->carrierRepository->getAnyById( (string) $carrierData['id'] ); if ( null !== $carrier && $carrier->supportsCod() ) { $form->addText( 'default_COD_surcharge', __( 'Default COD surcharge', 'packeta' ) . ':' ) ->setRequired( false ) ->addRule( Form::FLOAT ) ->addRule( Form::MIN, null, 0 ); $surchargeLimits = $form->addContainer( 'surcharge_limits' ); if ( ! empty( $carrierData['surcharge_limits'] ) ) { foreach ( $carrierData['surcharge_limits'] as $index => $limit ) { $this->addSurchargeLimit( $surchargeLimits, $index ); } } $roundingOptions = [ Rounder::DONT_ROUND => __( 'No rounding', 'packeta' ), Rounder::ROUND_DOWN => __( 'Always round down', 'packeta' ), Rounder::ROUND_UP => __( 'Always round up', 'packeta' ), ]; $form->addSelect( 'cod_rounding', __( 'COD rounding', 'packeta' ) . ':', $roundingOptions ) ->setDefaultValue( Rounder::DONT_ROUND ); } $item = $form->addText( 'free_shipping_limit', __( 'Free shipping limit', 'packeta' ) . ':' ); $item->addRule( $form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) ); $couponFreeShipping = $form->addContainer( 'coupon_free_shipping' ); $couponFreeShipping->addCheckbox( 'active', __( 'Apply free shipping coupon', 'packeta' ) ); $couponFreeShipping->addCheckbox( 'allow_for_fees', __( 'Apply free shipping coupon for fees', 'packeta' ) ) ->addConditionOn( $form['coupon_free_shipping']['active'], Form::FILLED ) ->toggle( $this->createCouponFreeShippingForFeesContainerId( $form ) ); $form->addHidden( 'id' )->setRequired(); $form->addSubmit( 'save' ); if ( false === $carrier->hasPickupPoints() && in_array( $carrier->getCountry(), Carrier::ADDRESS_VALIDATION_COUNTRIES, true ) ) { $addressValidationOptions = [ 'none' => __( 'No address validation', 'packeta' ), 'optional' => __( 'Optional address validation', 'packeta' ), 'required' => __( 'Required address validation', 'packeta' ), ]; $form->addSelect( 'address_validation', __( 'Address validation', 'packeta' ) . ':', $addressValidationOptions ) ->setDefaultValue( 'none' ); } if ( $carrier->supportsAgeVerification() ) { $form->addText( 'age_verification_fee', __( 'Age verification fee', 'packeta' ) . ':' ) ->setRequired( false ) ->addRule( Form::FLOAT ) ->addRule( Form::MIN, null, 0 ); } $form->onValidate[] = [ $this, 'validateOptions' ]; $form->onSuccess[] = [ $this, 'updateOptions' ]; if ( false === $carrierOptions ) { $carrierOptions = [ 'id' => $carrierData['id'], self::FORM_FIELD_NAME => $carrierData['name'], ]; } $form->setDefaults( $carrierOptions ); return $form; } /** * Creates form toggle ID for coupon free shipping. * * @param Form $form Form. * * @return string */ private function createCouponFreeShippingForFeesContainerId( Form $form ): string { return sprintf( '%s_apply_free_shipping_coupon_allow_for_fees', $form->getName() ); } /** * Creates settings form. * * @param array $carrierData Carrier data. * * @return Form */ private function createFormTemplate( array $carrierData ): Form { $optionId = OptionPrefixer::getOptionId( $carrierData['id'] ); $form = $this->formFactory->create( $optionId . '_template' ); $weightLimitsTemplate = $form->addContainer( 'weight_limits' ); $this->addWeightLimit( $weightLimitsTemplate, 0 ); $surchargeLimitsTemplate = $form->addContainer( 'surcharge_limits' ); $this->addSurchargeLimit( $surchargeLimitsTemplate, 0 ); return $form; } /** * Validates options. * * @param Form $form Form. */ public function validateOptions( Form $form ): void { if ( $form->hasErrors() ) { add_settings_error( '', '', esc_attr( __( 'Some carrier data is invalid', 'packeta' ) ) ); return; } $options = $form->getValues( 'array' ); if ( $this->featureFlag->isSplitActive() ) { $checkedVendors = $this->getCheckedVendors( $options ); if ( isset( $options['vendor_groups'] ) && count( $options['vendor_groups'] ) >= self::MINIMUM_CHECKED_VENDORS && count( $checkedVendors ) < self::MINIMUM_CHECKED_VENDORS ) { $vendorMessage = __( 'Check at least two types of pickup points or set corresponding separate carriers.', 'packeta' ); add_settings_error( 'vendor_groups', 'vendor_groups', esc_attr( $vendorMessage ) ); $form->addError( $vendorMessage ); } } $this->checkOverlapping( $form, $options, 'weight_limits', 'weight', __( 'Weight rules are overlapping, please fix them.', 'packeta' ) ); if ( isset( $options['surcharge_limits'] ) ) { $this->checkOverlapping( $form, $options, 'surcharge_limits', 'order_price', __( 'Surcharge rules are overlapping, please fix them.', 'packeta' ) ); } } /** * Saves carrier options. onSuccess callback. * * @param Form $form Form. * * @return void */ public function updateOptions( Form $form ): void { $options = $form->getValues( 'array' ); $newVendors = $this->getCheckedVendors( $options ); if ( $newVendors ) { $options['vendor_groups'] = $newVendors; } $options = $this->mergeNewLimits( $options, 'weight_limits' ); $options = $this->sortLimits( $options, 'weight_limits', 'weight' ); if ( isset( $options['surcharge_limits'] ) ) { $options = $this->mergeNewLimits( $options, 'surcharge_limits' ); $options = $this->sortLimits( $options, 'surcharge_limits', 'order_price' ); } update_option( OptionPrefixer::getOptionId( $options['id'] ), $options ); $this->messageManager->flash_message( __( 'Settings saved', 'packeta' ), MessageManager::TYPE_SUCCESS, MessageManager::RENDERER_PACKETERY, 'carrier-country' ); if ( wp_safe_redirect( add_query_arg( [ 'page' => self::SLUG, 'code' => $this->httpRequest->getQuery( 'code' ), ], get_admin_url( null, 'admin.php' ) ), 303 ) ) { exit; } } /** * Renders page. */ public function render(): void { $countryIso = $this->httpRequest->getQuery( 'code' ); if ( $countryIso ) { $countryCarriers = $this->carrierRepository->getByCountryIncludingNonFeed( $countryIso ); $carriersData = []; $post = $this->httpRequest->getPost(); foreach ( $countryCarriers as $carrier ) { if ( ! empty( $post ) && $post['id'] === $carrier->getId() ) { $formTemplate = $this->createFormTemplate( $post ); $form = $this->createForm( $post ); if ( $form->isSubmitted() ) { $form->fireEvents(); } } else { $carrierData = $carrier->__toArray(); $options = get_option( OptionPrefixer::getOptionId( $carrier->getId() ) ); if ( false !== $options ) { $carrierData += $options; } $formTemplate = $this->createFormTemplate( $carrierData ); $form = $this->createForm( $carrierData ); } $carriersData[] = [ 'form' => $form, 'formTemplate' => $formTemplate, 'carrier' => $carrier, 'couponFreeShippingForFeesContainerId' => $this->createCouponFreeShippingForFeesContainerId( $form ), ]; } $this->latteEngine->render( PACKETERY_PLUGIN_DIR . '/template/carrier/country.latte', [ 'forms' => $carriersData, 'country_iso' => $countryIso, 'globalCurrency' => get_woocommerce_currency_symbol(), 'flashMessages' => $this->messageManager->renderToString( MessageManager::RENDERER_PACKETERY, 'carrier-country' ), 'translations' => [ 'cannotUseThisCarrierBecauseRequiresCustomsDeclaration' => __( 'This carrier cannot be used, because it requires a customs declaration.', 'packeta' ), 'delete' => __( 'Delete', 'packeta' ), 'weightRules' => __( 'Weight rules', 'packeta' ), 'addWeightRule' => __( 'Add weight rule', 'packeta' ), 'codSurchargeRules' => __( 'COD surcharge rules', 'packeta' ), 'addCodSurchargeRule' => __( 'Add COD surcharge rule', 'packeta' ), 'afterExceedingThisAmountShippingIsFree' => __( 'After exceeding this amount, shipping is free.', 'packeta' ), 'addressValidationDescription' => __( 'Customer address validation.', 'packeta' ), 'roundingDescription' => __( 'COD rounding for submitting data to Packeta', 'packeta' ), 'saveChanges' => __( 'Save changes', 'packeta' ), 'packeta' => __( 'Packeta', 'packeta' ), // translators: %s is country code. 'title' => sprintf( __( 'Country options: %s', 'packeta' ), strtoupper( $countryIso ) ), 'noKnownCarrierForThisCountry' => __( 'No carriers available for this country.', 'packeta' ), 'ageVerificationSupportedNotification' => __( 'When shipping via this carrier, you can order the Age Verification service. The service will get ordered automatically if there is at least 1 product in the order with the age verification setting.', 'packeta' ), 'carrierDoesNotSupportCod' => __( 'This carrier does not support COD payment.', 'packeta' ), 'allowedPickupPointTypes' => __( 'Allowed pickup point types.', 'packeta' ), 'checkAtLeastTwo' => __( 'Check at least two types of pickup points or set corresponding separate carriers.', 'packeta' ), ], ] ); } else { $this->countryListingPage->render(); } } /** * Transforms new_ keys to common numeric. * * @param array $options Options to merge. * @param string $limitsContainer Container id. * * @return array */ private function mergeNewLimits( array $options, string $limitsContainer ): array { $newOptions = []; if ( isset( $options[ $limitsContainer ] ) ) { foreach ( $options[ $limitsContainer ] as $key => $option ) { if ( is_int( $key ) ) { $newOptions[ $key ] = $option; } if ( 0 === strpos( (string) $key, 'new_' ) ) { $newOptions[] = $option; } } $options[ $limitsContainer ] = $newOptions; } return $options; } /** * Checks rules overlapping. * * @param Form $form Form. * @param array $options Form data. * @param string $limitsContainer Container id. * @param string $limitKey Rule id. * @param string $overlappingMessage Error message. * * @return void */ private function checkOverlapping( Form $form, array $options, string $limitsContainer, string $limitKey, string $overlappingMessage ): void { $limits = array_column( $options[ $limitsContainer ], $limitKey ); if ( count( array_unique( $limits, SORT_NUMERIC ) ) !== count( $limits ) ) { add_settings_error( $limitsContainer, $limitsContainer, esc_attr( $overlappingMessage ) ); $form->addError( $overlappingMessage ); } } /** * Sorts rules. * * @param array $options Form data. * @param string $limitsContainer Container id. * @param string $limitKey Rule id. * * @return array */ private function sortLimits( array $options, string $limitsContainer, string $limitKey ): array { $limits = array_column( $options[ $limitsContainer ], $limitKey ); array_multisort( $limits, SORT_ASC, $options[ $limitsContainer ] ); return $options; } /** * Adds limit fields to form. * * @param Container $weightLimits Container. * @param int|string $index Index. * * @return void */ private function addWeightLimit( Container $weightLimits, $index ): void { $limit = $weightLimits->addContainer( (string) $index ); $item = $limit->addText( 'weight', __( 'Weight up to', 'packeta' ) . ':' ); $item->setRequired(); $item->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) ); // translators: %d is numeric threshold. $item->addRule( [ FormValidators::class, 'greaterThan' ], __( 'Enter number greater than %d', 'packeta' ), 0.0 ); $item->addFilter( function ( float $value ) { return Helper::simplifyWeight( $value ); } ); // translators: %d is numeric threshold. $item->addRule( [ FormValidators::class, 'greaterThan' ], __( 'Enter number greater than %d', 'packeta' ), 0.0 ); $item = $limit->addText( 'price', __( 'Price', 'packeta' ) . ':' ); $item->setRequired(); $item->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) ); $item->addRule( Form::MIN, null, 0 ); } /** * Adds limit fields to form. * * @param Container $surchargeLimits Container. * @param int|string $index Index. * * @return void */ private function addSurchargeLimit( Container $surchargeLimits, $index ): void { $limit = $surchargeLimits->addContainer( (string) $index ); $item = $limit->addText( 'order_price', __( 'Order price up to', 'packeta' ) . ':' ); $item->setRequired(); $item->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) ); $item->addRule( Form::MIN, null, 0 ); $item->addCondition( Form::MAX, 0 ) ->addCondition( Form::MIN, 0 ) // translators: %d is the value. ->addRule( Form::BLANK, __( 'Value must not be %d', 'packeta' ), 0 ); $item = $limit->addText( 'surcharge', __( 'Surcharge', 'packeta' ) . ':' ); $item->setRequired(); $item->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) ); $item->addRule( Form::MIN, null, 0 ); } /** * Creates link to page. * * @param string|null $countryCode Country code. * * @return string */ public function createUrl( ?string $countryCode = null ): string { $params = [ 'page' => self::SLUG, ]; if ( null !== $countryCode ) { $params['code'] = $countryCode; } return add_query_arg( $params, admin_url( 'admin.php' ) ); } /** * Gets checked vendors. * * @param array $options Form options. * * @return array */ private function getCheckedVendors( array $options ): array { $vendorCodes = []; if ( ! empty( $options['vendor_groups'] ) ) { $vendorCodes = $options['vendor_groups']; } $newVendors = []; foreach ( $vendorCodes as $vendorId => $isChecked ) { if ( ! $isChecked ) { continue; } $newVendors[] = $vendorId; } return $newVendors; } /** * Gets available vendors for compound carrier. * * @param string $id Compound carrier id. * * @return array|null */ private function getAvailableVendors( $id ): ?array { if ( ! $this->pickupPointsConfig->isCompoundCarrierId( $id ) ) { return null; } $compoundCarriers = $this->pickupPointsConfig->getCompoundCarriers(); foreach ( $compoundCarriers as $compoundCarrier ) { if ( $id === $compoundCarrier->getId() ) { return $compoundCarrier->getVendorCodes(); } } return null; } /** * Gets configuration of vendor checkboxes. * * @param string $carrierId Carrier id. * @param array|null $carrierOptions Carrier options. * * @return array */ private function getVendorCheckboxesConfig( string $carrierId, ?array $carrierOptions ): array { $availableVendors = $this->getAvailableVendors( $carrierId ); if ( null === $availableVendors ) { return []; } $vendorCheckboxes = []; $vendorCarriers = $this->pickupPointsConfig->getVendorCarriers(); foreach ( $availableVendors as $vendorId ) { $vendorProvider = $vendorCarriers[ $vendorId ]; $checkbox = [ 'group' => $vendorProvider->getGroup(), 'name' => $vendorProvider->getName(), 'disabled' => null, 'default' => null, ]; $hasLowCountAvailable = count( $availableVendors ) <= self::MINIMUM_CHECKED_VENDORS; if ( $hasLowCountAvailable ) { $checkbox['disabled'] = true; } $hasGroupSettingsSaved = isset( $carrierOptions['vendor_groups'] ); $hasTheGroupAllowed = ( $hasGroupSettingsSaved && in_array( $vendorProvider->getGroup(), $carrierOptions['vendor_groups'], true ) ); if ( ! $hasGroupSettingsSaved || $hasLowCountAvailable || $hasTheGroupAllowed ) { $checkbox['default'] = true; } $vendorCheckboxes[] = $checkbox; } return $vendorCheckboxes; } }