PluginProbe
Packeta / 2.0.9
Packeta v2.0.9
2.3.2 2.3.1 trunk 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 All 56 releases
packeta / src / Packetery / Module / Carrier / OptionsPage.php

OptionsPage.php in Packeta 2.0.9, at src/Packetery/Module/Carrier/OptionsPage.php

1,006 lines 35.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class OptionsPage
4 *
5 * @package Packetery\Options
6 */
7
8 declare( strict_types=1 );
9
10 namespace Packetery\Module\Carrier;
11
12 use Packetery\Core\CoreHelper;
13 use Packetery\Core\Entity\Carrier;
14 use Packetery\Core\Rounder;
15 use Packetery\Latte\Engine;
16 use Packetery\Module\FormFactory;
17 use Packetery\Module\FormValidators;
18 use Packetery\Module\Framework\WpAdapter;
19 use Packetery\Module\MessageManager;
20 use Packetery\Module\ModuleHelper;
21 use Packetery\Module\Options\FlagManager\FeatureFlagProvider;
22 use Packetery\Module\Options\Page;
23 use Packetery\Module\PaymentGatewayHelper;
24 use Packetery\Module\Views\UrlBuilder;
25 use Packetery\Nette\Forms\Container;
26 use Packetery\Nette\Forms\Control;
27 use Packetery\Nette\Forms\Form;
28 use Packetery\Nette\Http\Request;
29
30 /**
31 * Class OptionsPage
32 *
33 * @package Packetery\Options
34 */
35 class OptionsPage {
36
37 public const FORM_FIELD_NAME = 'name';
38 public const FORM_FIELD_ACTIVE = 'active';
39 public const FORM_FIELD_WEIGHT_LIMITS = 'weight_limits';
40 public const FORM_FIELD_PRODUCT_VALUE_LIMITS = 'product_value_limits';
41 public const FORM_FIELD_PRICING_TYPE = 'pricing_type';
42
43 public const SLUG = 'packeta-country';
44 public const PARAMETER_COUNTRY_CODE = 'country_code';
45 public const PARAMETER_CARRIER_ID = 'carrier_id';
46 public const MINIMUM_CHECKED_VENDORS = 2;
47
48 /**
49 * @var Engine
50 */
51 private $latteEngine;
52
53 /**
54 * @var EntityRepository
55 */
56 private $carrierRepository;
57
58 /**
59 * @var FormFactory
60 */
61 private $formFactory;
62
63 /**
64 * @var Request
65 */
66 private $httpRequest;
67
68 /**
69 * @var CountryListingPage
70 */
71 private $countryListingPage;
72
73 /**
74 * @var MessageManager
75 */
76 private $messageManager;
77
78 /**
79 * @var PacketaPickupPointsConfig
80 */
81 private $pickupPointsConfig;
82
83 /**
84 * @var FeatureFlagProvider
85 */
86 private $featureFlagProvider;
87
88 /**
89 * @var CarDeliveryConfig
90 */
91 private $carDeliveryConfig;
92
93 /**
94 * @var CarrierOptionsFactory
95 */
96 private $carrierOptionsFactory;
97
98 /**
99 * @var ModuleHelper
100 */
101 private $moduleHelper;
102
103 /**
104 * @var UrlBuilder
105 */
106 private $urlBuilder;
107
108 /**
109 * @var WpAdapter
110 */
111 private $wpAdapter;
112
113 public function __construct(
114 Engine $latteEngine,
115 EntityRepository $carrierRepository,
116 FormFactory $formFactory,
117 Request $httpRequest,
118 CountryListingPage $countryListingPage,
119 MessageManager $messageManager,
120 PacketaPickupPointsConfig $pickupPointsConfig,
121 FeatureFlagProvider $featureFlagProvider,
122 CarDeliveryConfig $carDeliveryConfig,
123 CarrierOptionsFactory $carrierOptionsFactory,
124 ModuleHelper $moduleHelper,
125 UrlBuilder $urlBuilder,
126 WpAdapter $wpAdapter
127 ) {
128 $this->latteEngine = $latteEngine;
129 $this->carrierRepository = $carrierRepository;
130 $this->formFactory = $formFactory;
131 $this->httpRequest = $httpRequest;
132 $this->countryListingPage = $countryListingPage;
133 $this->messageManager = $messageManager;
134 $this->pickupPointsConfig = $pickupPointsConfig;
135 $this->featureFlagProvider = $featureFlagProvider;
136 $this->carDeliveryConfig = $carDeliveryConfig;
137 $this->carrierOptionsFactory = $carrierOptionsFactory;
138 $this->moduleHelper = $moduleHelper;
139 $this->urlBuilder = $urlBuilder;
140 $this->wpAdapter = $wpAdapter;
141 }
142
143 /**
144 * Registers WP callbacks.
145 */
146 public function register(): void {
147 add_submenu_page(
148 Page::SLUG,
149 __( 'Carrier settings', 'packeta' ),
150 __( 'Carrier settings', 'packeta' ),
151 'manage_options',
152 self::SLUG,
153 array(
154 $this,
155 'render',
156 ),
157 10
158 );
159 }
160
161 /**
162 * Creates settings form.
163 *
164 * @param array $carrierData Carrier data.
165 *
166 * @return Form
167 */
168 private function createForm( array $carrierData ): Form {
169 $optionId = OptionPrefixer::getOptionId( $carrierData['id'] );
170
171 $form = $this->formFactory->create( $optionId );
172
173 $form->addCheckbox(
174 self::FORM_FIELD_ACTIVE,
175 __( 'Active carrier', 'packeta' ) . ':'
176 );
177
178 $form->addText( self::FORM_FIELD_NAME, __( 'Display name', 'packeta' ) . ':' )
179 ->setRequired();
180
181 $carrierOptions = get_option( $optionId );
182 if ( $this->featureFlagProvider->isSplitActive() ) {
183 $vendorCheckboxes = $this->getVendorCheckboxesConfig( $carrierData['id'], ( $carrierOptions !== false ? $carrierOptions : null ) );
184 if ( count( $vendorCheckboxes ) > 0 ) {
185 $vendorsContainer = $form->addContainer( 'vendor_groups' );
186 foreach ( $vendorCheckboxes as $checkboxConfig ) {
187 $checkboxControl = $vendorsContainer->addCheckbox( $checkboxConfig['group'], $checkboxConfig['name'] );
188 if ( $checkboxConfig['disabled'] === true ) {
189 $checkboxControl->setDisabled()->setOmitted( false );
190 }
191 if ( $checkboxConfig['default'] === true ) {
192 $checkboxControl->setDefaultValue( true );
193 }
194 }
195 }
196 }
197
198 $form->addSelect(
199 self::FORM_FIELD_PRICING_TYPE,
200 __( 'Pricing type', 'packeta' ),
201 [
202 Options::PRICING_TYPE_BY_WEIGHT => __( 'By weight', 'packeta' ),
203 Options::PRICING_TYPE_BY_PRODUCT_VALUE => __( 'By product value', 'packeta' ),
204 ]
205 )
206 ->setDefaultValue( Options::PRICING_TYPE_BY_WEIGHT )
207 ->addCondition( Form::EQUAL, Options::PRICING_TYPE_BY_WEIGHT )
208 ->toggle( $this->createFieldContainerId( $form, self::FORM_FIELD_WEIGHT_LIMITS ) )
209 ->endCondition()
210 ->addCondition( Form::EQUAL, Options::PRICING_TYPE_BY_PRODUCT_VALUE )
211 ->toggle( $this->createFieldContainerId( $form, self::FORM_FIELD_PRODUCT_VALUE_LIMITS ) );
212
213 $weightLimits = $form->addContainer( self::FORM_FIELD_WEIGHT_LIMITS );
214 if ( ! isset( $carrierData[ self::FORM_FIELD_WEIGHT_LIMITS ] ) || count( $carrierData[ self::FORM_FIELD_WEIGHT_LIMITS ] ) === 0 ) {
215 $this->addWeightLimit( $weightLimits, 0 );
216 } else {
217 foreach ( $carrierData[ self::FORM_FIELD_WEIGHT_LIMITS ] as $index => $limit ) {
218 $this->addWeightLimit( $weightLimits, $index );
219 }
220 }
221
222 $productValueLimits = $form->addContainer( self::FORM_FIELD_PRODUCT_VALUE_LIMITS );
223 if ( ! isset( $carrierData[ self::FORM_FIELD_PRODUCT_VALUE_LIMITS ] ) || count( $carrierData[ self::FORM_FIELD_PRODUCT_VALUE_LIMITS ] ) === 0 ) {
224 $this->addProductValueLimit( $productValueLimits, 0 );
225 } else {
226 foreach ( $carrierData[ self::FORM_FIELD_PRODUCT_VALUE_LIMITS ] as $index => $limit ) {
227 $this->addProductValueLimit( $productValueLimits, $index );
228 }
229 }
230
231 // We don't expect id to be empty in this situation. This would indicate a data save error.
232 $carrier = $this->carrierRepository->getAnyById( (string) $carrierData['id'] );
233
234 if ( $carrier !== null && $carrier->supportsCod() ) {
235 $form->addText( 'default_COD_surcharge', __( 'Default COD surcharge', 'packeta' ) . ':' )
236 ->setRequired( false )
237 ->addRule( Form::FLOAT )
238 ->addRule( Form::MIN, null, 0 );
239
240 $surchargeLimits = $form->addContainer( 'surcharge_limits' );
241 if ( isset( $carrierData['surcharge_limits'] ) && count( $carrierData['surcharge_limits'] ) > 0 ) {
242 foreach ( $carrierData['surcharge_limits'] as $index => $limit ) {
243 $this->addSurchargeLimit( $surchargeLimits, $index );
244 }
245 }
246 $roundingOptions = [
247 Rounder::DONT_ROUND => __( 'No rounding', 'packeta' ),
248 Rounder::ROUND_DOWN => __( 'Always round down', 'packeta' ),
249 Rounder::ROUND_UP => __( 'Always round up', 'packeta' ),
250 ];
251 $form->addSelect( 'cod_rounding', __( 'COD rounding', 'packeta' ) . ':', $roundingOptions )
252 ->setDefaultValue( Rounder::DONT_ROUND );
253 }
254
255 $item = $form->addText( 'free_shipping_limit', __( 'Free shipping limit', 'packeta' ) . ':' );
256 $item->setRequired( false )
257 ->addRule( Form::FLOAT )
258 ->addRule( Form::MIN, null, 0 );
259
260 if ( $carrier !== null && $carrier->isCarDelivery() ) {
261 $daysUntilShipping = $form->addText( 'days_until_shipping', __( 'Number of days until shipping', 'packeta' ) . ':' );
262 $daysUntilShipping->setRequired()
263 ->addRule( $form::INTEGER, __( 'Please, enter a full number.', 'packeta' ) )
264 ->addRule( $form::MIN, null, 0 );
265
266 $shippingTimeCutOff = $form->addText( 'shipping_time_cut_off', __( 'Shipping time cut off', 'packeta' ) . ':' );
267 $shippingTimeCutOff->setHtmlAttribute( 'class', 'date-picker' )
268 ->setHtmlType( 'time' )
269 ->setRequired( false )
270 ->setNullable()
271 // translators: %s: Represents the time we stop taking more orders for next shipment.
272 ->addRule( [ FormValidators::class, 'hasClockTimeFormat' ], __( 'Time must be between %1$s and %2$s.', 'packeta' ), [ '00:00', '23:59' ] );
273 }
274
275 $couponFreeShipping = $form->addContainer( 'coupon_free_shipping' );
276 $couponFreeShipping->addCheckbox( 'active', __( 'Apply free shipping coupon', 'packeta' ) );
277 $couponFreeShipping->addCheckbox( 'allow_for_fees', __( 'Apply free shipping coupon for fees', 'packeta' ) )
278 ->addConditionOn( $form['coupon_free_shipping']['active'], Form::FILLED )
279 ->toggle( $this->createCouponFreeShippingForFeesContainerId( $form ) );
280
281 $dimensionsRestrictions = $form->addContainer( 'dimensions_restrictions' );
282 $dimensionsRestrictions->addCheckbox( 'active', $this->wpAdapter->__( 'Maximum package size', 'packeta' ) );
283 if (
284 strpos( $carrier->getId(), Carrier::VENDOR_GROUP_ZBOX ) !== false ||
285 strpos( $carrier->getId(), Carrier::VENDOR_GROUP_ZPOINT ) === 0 ||
286 ( $carrier->hasPickupPoints() && is_numeric( $carrier->getId() ) )
287 ) {
288 $dimensionsRestrictions->addText( 'length', $this->wpAdapter->__( 'Length (cm)', 'packeta' ) )
289 ->setNullable()
290 ->addConditionOn( $form['dimensions_restrictions']['active'], Form::FILLED )
291 ->toggle( $this->createDimensionRestrictionContainerId( $form ) )
292 ->setRequired()
293 ->addRule( Form::INTEGER, $this->wpAdapter->__( 'Provide a full number!', 'packeta' ) )
294 ->addRule( Form::MIN, 'Value must be greater than 0', 1 );
295 $dimensionsRestrictions->addText( 'width', $this->wpAdapter->__( 'Width (cm)', 'packeta' ) )
296 ->setNullable()
297 ->addConditionOn( $form['dimensions_restrictions']['active'], Form::FILLED )
298 ->toggle( $this->createDimensionRestrictionContainerId( $form ) )
299 ->setRequired()
300 ->addRule( Form::INTEGER, $this->wpAdapter->__( 'Provide a full number!', 'packeta' ) )
301 ->addRule( Form::MIN, 'Value must be greater than 0', 1 );
302 $dimensionsRestrictions->addText( 'height', $this->wpAdapter->__( 'Height (cm)', 'packeta' ) )
303 ->setNullable()
304 ->addConditionOn( $form['dimensions_restrictions']['active'], Form::FILLED )
305 ->toggle( $this->createDimensionRestrictionContainerId( $form ) )
306 ->setRequired()
307 ->addRule( Form::INTEGER, $this->wpAdapter->__( 'Provide a full number!', 'packeta' ) )
308 ->addRule( Form::MIN, 'Value must be greater than 0', 1 );
309 } else {
310
311 $maximumLength = $dimensionsRestrictions->addText( 'maximum_length', $this->wpAdapter->__( 'Maximum length (cm)', 'packeta' ) );
312 $maximumLength->setNullable();
313 $maximumLength->addConditionOn( $form['dimensions_restrictions']['active'], Form::FILLED )
314 ->toggle( $this->createDimensionRestrictionContainerId( $form ) )
315 ->addRule( Form::INTEGER, $this->wpAdapter->__( 'Provide a full number!', 'packeta' ) )
316 ->addRule( Form::MIN, $this->wpAdapter->__( 'Value must be greater than 0', 'packeta' ), 1 );
317
318 $dimensionsSum = $dimensionsRestrictions->addText( 'dimensions_sum', $this->wpAdapter->__( 'Sum of dimensions (cm)', 'packeta' ) );
319 $dimensionsSum->setNullable();
320 $dimensionsSum->addConditionOn( $form['dimensions_restrictions']['active'], Form::FILLED )
321 ->toggle( $this->createDimensionRestrictionContainerId( $form ) )
322 ->addRule( Form::INTEGER, $this->wpAdapter->__( 'Provide a full number!', 'packeta' ) )
323 ->addRule( Form::MIN, $this->wpAdapter->__( 'Value must be greater than 0', 'packeta' ), 1 );
324
325 $maximumLength->addConditionOn( $form['dimensions_restrictions']['active'], Form::FILLED )
326 ->addConditionOn( $dimensionsSum, Form::BLANK )
327 ->addRule( Form::FILLED, $this->wpAdapter->__( 'You have to fill in Maximum length or Sum of dimensions', 'packeta' ) );
328
329 $dimensionsSum->addConditionOn( $form['dimensions_restrictions']['active'], Form::FILLED )
330 ->addConditionOn( $maximumLength, Form::BLANK )
331 ->addRule( Form::FILLED, $this->wpAdapter->__( 'You have to fill in Maximum length or Sum of dimensions', 'packeta' ) );
332 }
333
334 $form->addHidden( 'id' )->setRequired();
335 $form->addSubmit( 'save' );
336
337 if (
338 $carrier->isCarDelivery() === false &&
339 $carrier->hasPickupPoints() === false &&
340 in_array( $carrier->getCountry(), Carrier::ADDRESS_VALIDATION_COUNTRIES, true )
341 ) {
342 $addressValidationOptions = [
343 'none' => __( 'No address validation', 'packeta' ),
344 'optional' => __( 'Optional address validation', 'packeta' ),
345 'required' => __( 'Required address validation', 'packeta' ),
346 ];
347 $form->addSelect( 'address_validation', __( 'Address validation', 'packeta' ) . ':', $addressValidationOptions )
348 ->setDefaultValue( 'none' );
349 }
350
351 if ( $carrier->supportsAgeVerification() ) {
352 $form->addText( 'age_verification_fee', __( 'Age verification fee', 'packeta' ) . ':' )
353 ->setRequired( false )
354 ->addRule( Form::FLOAT )
355 ->addRule( Form::MIN, null, 0 );
356 }
357
358 $form->addMultiSelect(
359 'disallowed_checkout_payment_methods',
360 __( 'Disallowed checkout payment methods', 'packeta' ),
361 PaymentGatewayHelper::getAvailablePaymentGatewayChoices()
362 )->checkDefaultValue( false );
363
364 $form->onValidate[] = [ $this, 'validateOptions' ];
365 $form->onSuccess[] = [ $this, 'updateOptions' ];
366
367 if ( $carrierOptions === false ) {
368 $carrierOptions = [
369 'id' => $carrierData['id'],
370 self::FORM_FIELD_NAME => $carrierData['name'],
371 ];
372 }
373 $form->setDefaults( $carrierOptions );
374
375 return $form;
376 }
377
378 /**
379 * Creates form toggle ID for coupon free shipping.
380 *
381 * @param Form $form Form.
382 *
383 * @return string
384 */
385 private function createCouponFreeShippingForFeesContainerId( Form $form ): string {
386 return sprintf( '%s_apply_free_shipping_coupon_allow_for_fees', $form->getName() );
387 }
388
389 private function createDimensionRestrictionContainerId( Form $form ): string {
390 return sprintf( '%s_dimension_restrictions', $form->getName() );
391 }
392
393 /**
394 * Creates container id for given field.
395 *
396 * @param Form $form Form.
397 * @param string $field Field name.
398 *
399 * @return string
400 */
401 private function createFieldContainerId( Form $form, string $field ): string {
402 return sprintf( '%s_%s_containerId', $form->getName(), $field );
403 }
404
405 /**
406 * Creates settings form.
407 *
408 * @param array $carrierData Carrier data.
409 *
410 * @return Form
411 */
412 private function createFormTemplate( array $carrierData ): Form {
413 $optionId = OptionPrefixer::getOptionId( $carrierData['id'] );
414
415 $form = $this->formFactory->create( $optionId . '_template' );
416
417 $form->addSelect( self::FORM_FIELD_PRICING_TYPE );
418
419 $weightLimitsTemplate = $form->addContainer( self::FORM_FIELD_WEIGHT_LIMITS );
420 $this->addWeightLimit( $weightLimitsTemplate, 0 );
421
422 $valueLimitsTemplate = $form->addContainer( self::FORM_FIELD_PRODUCT_VALUE_LIMITS );
423 $this->addProductValueLimit( $valueLimitsTemplate, 0 );
424
425 $surchargeLimitsTemplate = $form->addContainer( 'surcharge_limits' );
426 $this->addSurchargeLimit( $surchargeLimitsTemplate, 0 );
427
428 return $form;
429 }
430
431 /**
432 * Validates options.
433 *
434 * @param Form $form Form.
435 */
436 public function validateOptions( Form $form ): void {
437 if ( $form->hasErrors() ) {
438 add_settings_error( '', '', esc_attr( __( 'Some carrier data is invalid', 'packeta' ) ) );
439
440 return;
441 }
442
443 $options = $form->getValues( 'array' );
444
445 if ( $this->featureFlagProvider->isSplitActive() ) {
446 $checkedVendors = $this->getCheckedVendors( $options );
447 if (
448 isset( $options['vendor_groups'] ) &&
449 count( $options['vendor_groups'] ) >= self::MINIMUM_CHECKED_VENDORS &&
450 count( $checkedVendors ) < self::MINIMUM_CHECKED_VENDORS
451 ) {
452 $vendorMessage = __( 'Check at least two types of pickup points or use a carrier which delivers to the desired pickup point type.', 'packeta' );
453 add_settings_error( 'vendor_groups', 'vendor_groups', esc_attr( $vendorMessage ) );
454 $form->addError( $vendorMessage );
455 }
456 }
457
458 if ( $options[ self::FORM_FIELD_PRICING_TYPE ] === Options::PRICING_TYPE_BY_WEIGHT ) {
459 $this->checkOverlapping(
460 $form,
461 $options,
462 self::FORM_FIELD_WEIGHT_LIMITS,
463 'weight',
464 __( 'Weight rules are overlapping, please fix them.', 'packeta' )
465 );
466 }
467
468 if ( $options[ self::FORM_FIELD_PRICING_TYPE ] === Options::PRICING_TYPE_BY_PRODUCT_VALUE ) {
469 $this->checkOverlapping(
470 $form,
471 $options,
472 self::FORM_FIELD_PRODUCT_VALUE_LIMITS,
473 'price',
474 __( 'Product price rules are overlapping, please fix them.', 'packeta' )
475 );
476 }
477
478 if ( isset( $options['surcharge_limits'] ) ) {
479 $this->checkOverlapping(
480 $form,
481 $options,
482 'surcharge_limits',
483 'order_price',
484 __( 'Surcharge rules are overlapping, please fix them.', 'packeta' )
485 );
486 }
487 }
488
489 /**
490 * Saves carrier options. onSuccess callback.
491 *
492 * @param Form $form Form.
493 *
494 * @return void
495 */
496 public function updateOptions( Form $form ): void {
497 $options = $form->getValues( 'array' );
498 $newVendors = $this->getCheckedVendors( $options );
499 if ( count( $newVendors ) > 0 ) {
500 $options['vendor_groups'] = $newVendors;
501 }
502
503 $persistedOptions = $this->carrierOptionsFactory->createByCarrierId( $options['id'] );
504 $persistedOptionsArray = $persistedOptions->toArray();
505
506 if ( $options[ self::FORM_FIELD_PRICING_TYPE ] === Options::PRICING_TYPE_BY_WEIGHT ) {
507 $options = $this->mergeNewLimits( $options, self::FORM_FIELD_WEIGHT_LIMITS );
508 $options = $this->sortLimits( $options, self::FORM_FIELD_WEIGHT_LIMITS, 'weight' );
509
510 $options[ self::FORM_FIELD_PRODUCT_VALUE_LIMITS ] = $persistedOptionsArray[ self::FORM_FIELD_PRODUCT_VALUE_LIMITS ] ?? [];
511 }
512
513 if ( $options[ self::FORM_FIELD_PRICING_TYPE ] === Options::PRICING_TYPE_BY_PRODUCT_VALUE ) {
514 $options = $this->mergeNewLimits( $options, self::FORM_FIELD_PRODUCT_VALUE_LIMITS );
515 $options = $this->sortLimits( $options, self::FORM_FIELD_PRODUCT_VALUE_LIMITS, 'value' );
516
517 $options[ self::FORM_FIELD_WEIGHT_LIMITS ] = $persistedOptionsArray[ self::FORM_FIELD_WEIGHT_LIMITS ] ?? [];
518 }
519
520 if ( isset( $options['surcharge_limits'] ) ) {
521 $options = $this->mergeNewLimits( $options, 'surcharge_limits' );
522 $options = $this->sortLimits( $options, 'surcharge_limits', 'order_price' );
523 }
524
525 update_option( OptionPrefixer::getOptionId( $options['id'] ), $options );
526 $this->messageManager->flash_message( __( 'Settings saved', 'packeta' ), MessageManager::TYPE_SUCCESS, MessageManager::RENDERER_PACKETERY, 'carrier-country' );
527
528 if ( wp_safe_redirect(
529 $this->createUrl(
530 $this->httpRequest->getQuery( self::PARAMETER_COUNTRY_CODE ),
531 $this->httpRequest->getQuery( self::PARAMETER_CARRIER_ID )
532 ),
533 303
534 ) ) {
535 exit;
536 }
537 }
538
539 /**
540 * Gets carrier template params.
541 *
542 * @param Carrier|null $carrier Carrier.
543 *
544 * @return array|null
545 */
546 private function getCarrierTemplateData( ?Carrier $carrier ): ?array {
547 if ( $carrier === null ) {
548 return null;
549 }
550
551 if ( $carrier->isCarDelivery() && $this->carDeliveryConfig->isDisabled() ) {
552 return null;
553 }
554
555 $post = $this->httpRequest->getPost();
556 if ( isset( $post['id'] ) && $post['id'] === $carrier->getId() ) {
557 $formTemplate = $this->createFormTemplate( $post );
558 $form = $this->createForm( $post );
559 if ( $form->isSubmitted() ) {
560 $form->fireEvents();
561 }
562 } else {
563 $carrierData = $carrier->__toArray();
564 $options = get_option( OptionPrefixer::getOptionId( $carrier->getId() ) );
565 if ( $options !== false ) {
566 $carrierData += $options;
567 }
568 $formTemplate = $this->createFormTemplate( $carrierData );
569 $form = $this->createForm( $carrierData );
570 }
571
572 return [
573 'form' => $form,
574 'formTemplate' => $formTemplate,
575 'carrier' => $carrier,
576 'couponFreeShippingForFeesContainerId' => $this->createCouponFreeShippingForFeesContainerId( $form ),
577 'dimensionRestrictionContainerId' => $this->createDimensionRestrictionContainerId( $form ),
578 'weightLimitsContainerId' => $this->createFieldContainerId( $form, self::FORM_FIELD_WEIGHT_LIMITS ),
579 'productValueLimitsContainerId' => $this->createFieldContainerId( $form, self::FORM_FIELD_PRODUCT_VALUE_LIMITS ),
580 'isAvailableVendorsCountLow' => $this->isAvailableVendorsCountLowByCarrierId( $carrier->getId() ),
581 ];
582 }
583
584 public function render(): void {
585 $countryIso = $this->httpRequest->getQuery( self::PARAMETER_COUNTRY_CODE );
586 $carrierId = $this->httpRequest->getQuery( self::PARAMETER_CARRIER_ID );
587
588 if ( $carrierId !== null ) {
589 $this->renderCarrierDetail( $carrierId );
590 } elseif ( $countryIso !== null ) {
591 $this->renderCountryCarriers( $countryIso );
592 } else {
593 $this->countryListingPage->render();
594 }
595 }
596
597 private function renderCarrierDetail( string $carrierId ): void {
598 $carrier = $this->carrierRepository->getAnyById( $carrierId );
599 $carrierTemplateData = $this->getCarrierTemplateData( $carrier );
600
601 if ( $carrier === null || $carrierTemplateData === null ) {
602 $this->countryListingPage->render();
603
604 return;
605 }
606
607 $this->latteEngine->render(
608 PACKETERY_PLUGIN_DIR . '/template/carrier/detail.latte',
609 array_merge_recursive(
610 $this->getCommonTemplateParams(),
611 [
612 'carrierTemplateData' => $carrierTemplateData,
613 'translations' => [
614 'title' => $carrier->getName(),
615 ],
616 ]
617 )
618 );
619 }
620
621 private function renderCountryCarriers( string $countryIso ): void {
622 $countryCarriers = $this->carrierRepository->getByCountryIncludingNonFeed( $countryIso );
623 $carriersData = [];
624 foreach ( $countryCarriers as $carrier ) {
625 $carrierTemplateData = $this->getCarrierTemplateData( $carrier );
626 if ( $carrierTemplateData !== null ) {
627 $carriersData[] = $carrierTemplateData;
628 }
629 }
630
631 $this->latteEngine->render(
632 PACKETERY_PLUGIN_DIR . '/template/carrier/country.latte',
633 array_merge_recursive(
634 $this->getCommonTemplateParams(),
635 [
636 'forms' => $carriersData,
637 'country_iso' => $countryIso,
638 'translations' => [
639 'title' => sprintf(
640 // translators: %s is country code.
641 $this->wpAdapter->__( 'Country options: %s', 'packeta' ),
642 strtoupper( $countryIso )
643 ),
644 ],
645 ]
646 )
647 );
648 }
649
650 /**
651 * @return array<string, null|string|bool|array<string, string>>
652 */
653 private function getCommonTemplateParams(): array {
654 return [
655 'globalCurrency' => get_woocommerce_currency_symbol(),
656 'flashMessages' => $this->messageManager->renderToString( MessageManager::RENDERER_PACKETERY, 'carrier-country' ),
657 'isCzechLocale' => $this->moduleHelper->isCzechLocale(),
658 'logoZasilkovna' => $this->urlBuilder->buildAssetUrl( 'public/images/logo-zasilkovna.svg' ),
659 'logoPacketa' => $this->urlBuilder->buildAssetUrl( 'public/images/logo-packeta.svg' ),
660 'translations' => $this->getTranslations(),
661 ];
662 }
663
664 /**
665 * @return array<string, string>
666 */
667 private function getTranslations(): array {
668 return [
669 'cannotUseThisCarrierBecauseRequiresCustomsDeclaration' => $this->wpAdapter->__( 'This carrier cannot be used, because it requires a customs declaration.', 'packeta' ),
670 'delete' => $this->wpAdapter->__( 'Delete', 'packeta' ),
671 'weightRules' => $this->wpAdapter->__( 'Weight rules', 'packeta' ),
672 'productValueRules' => $this->wpAdapter->__( 'Product value rules', 'packeta' ),
673 'addWeightRule' => $this->wpAdapter->__( 'Add weight rule', 'packeta' ),
674 'addProductValueRule' => $this->wpAdapter->__( 'Add product value rule', 'packeta' ),
675 'codSurchargeRules' => $this->wpAdapter->__( 'COD surcharge rules', 'packeta' ),
676 'addCodSurchargeRule' => $this->wpAdapter->__( 'Add COD surcharge rule', 'packeta' ),
677 'afterExceedingThisAmountShippingIsFree' => $this->wpAdapter->__( 'After exceeding this amount, shipping is free.', 'packeta' ),
678 'daysUntilShipping' => $this->wpAdapter->__( 'Number of business days it might take to process an order before shipping out a package.', 'packeta' ),
679 'shippingTimeCutOff' => $this->wpAdapter->__( 'A time of a day you stop taking in more orders for the next round of shipping.', 'packeta' ),
680 'addressValidationDescription' => $this->wpAdapter->__( 'Customer address validation.', 'packeta' ),
681 'roundingDescription' => $this->wpAdapter->__( 'COD rounding for submitting data to Packeta', 'packeta' ),
682 'saveChanges' => $this->wpAdapter->__( 'Save changes', 'packeta' ),
683 'packeta' => $this->wpAdapter->__( 'Packeta', 'packeta' ),
684 'noKnownCarrierForThisCountry' => $this->wpAdapter->__( 'No carriers available for this country.', 'packeta' ),
685 'ageVerificationSupportedNotification' => $this->wpAdapter->__( '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' ),
686 'carrierDoesNotSupportCod' => $this->wpAdapter->__( 'This carrier does not support COD payment.', 'packeta' ),
687 'allowedPickupPointTypes' => $this->wpAdapter->__( 'Pickup point types', 'packeta' ),
688 'checkAtLeastTwo' => $this->wpAdapter->__( 'Check at least two types of pickup points or use a carrier which delivers to the desired pickup point type.', 'packeta' ),
689 'lowAvailableVendorsCount' => $this->wpAdapter->__( 'This carrier displays all types of pickup points at the same time in the checkout (retail store pickup points, Z-boxes).', 'packeta' ),
690 ];
691 }
692
693 /**
694 * Transforms new_ keys to common numeric.
695 *
696 * @param array $options Options to merge.
697 * @param string $limitsContainer Container id.
698 *
699 * @return array
700 */
701 private function mergeNewLimits( array $options, string $limitsContainer ): array {
702 $newOptions = [];
703 if ( isset( $options[ $limitsContainer ] ) ) {
704 foreach ( $options[ $limitsContainer ] as $key => $option ) {
705 if ( is_int( $key ) ) {
706 $newOptions[ $key ] = $option;
707 }
708 if ( strpos( (string) $key, 'new_' ) === 0 ) {
709 $newOptions[] = $option;
710 }
711 }
712 $options[ $limitsContainer ] = $newOptions;
713 }
714
715 return $options;
716 }
717
718 /**
719 * Checks rules overlapping.
720 *
721 * @param Form $form Form.
722 * @param array $options Form data.
723 * @param string $limitsContainer Container id.
724 * @param string $limitKey Rule id.
725 * @param string $overlappingMessage Error message.
726 *
727 * @return void
728 */
729 private function checkOverlapping( Form $form, array $options, string $limitsContainer, string $limitKey, string $overlappingMessage ): void {
730 $limits = array_column( $options[ $limitsContainer ], $limitKey );
731 if ( count( array_unique( $limits, SORT_NUMERIC ) ) !== count( $limits ) ) {
732 add_settings_error( $limitsContainer, $limitsContainer, esc_attr( $overlappingMessage ) );
733 $form->addError( $overlappingMessage );
734 }
735 }
736
737 /**
738 * Sorts rules.
739 *
740 * @param array $options Form data.
741 * @param string $limitsContainer Container id.
742 * @param string $limitKey Rule id.
743 *
744 * @return array
745 */
746 private function sortLimits( array $options, string $limitsContainer, string $limitKey ): array {
747 $limits = array_column( $options[ $limitsContainer ], $limitKey );
748 array_multisort( $limits, SORT_ASC, $options[ $limitsContainer ] );
749
750 return $options;
751 }
752
753 /**
754 * @param Container $weightLimits
755 * @param int|string $index
756 *
757 * @throws \RuntimeException
758 */
759 private function addWeightLimit( Container $weightLimits, $index ): void {
760 $form = $weightLimits->getForm();
761 if ( $form === null ) {
762 throw new \RuntimeException( 'Form is not attached to the container.' );
763 }
764
765 $pricingTypeComponent = $form->getComponent( self::FORM_FIELD_PRICING_TYPE, false );
766
767 if ( ! $pricingTypeComponent instanceof Control ) {
768 throw new \RuntimeException(
769 sprintf(
770 'Component "%s" must be an instance of %s, %s given.',
771 self::FORM_FIELD_PRICING_TYPE,
772 Control::class,
773 is_object( $pricingTypeComponent ) ? get_class( $pricingTypeComponent ) : gettype( $pricingTypeComponent )
774 )
775 );
776 }
777
778 $limit = $weightLimits->addContainer( (string) $index );
779 $weightField = $limit->addText( 'weight', __( 'Weight up to', 'packeta' ) . ':' );
780 $weightRules = $weightField->addConditionOn( $pricingTypeComponent, Form::EQUAL, Options::PRICING_TYPE_BY_WEIGHT );
781 $weightRules->setRequired();
782 $weightRules->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) );
783 $weightRules->addRule( Form::MIN, null, 0 );
784 // translators: %d is numeric threshold.
785 $weightRules->addRule( [ FormValidators::class, 'greaterThan' ], __( 'Enter number greater than %d', 'packeta' ), 0.0 );
786
787 $weightRules->addFilter(
788 function ( float $value ) {
789 return CoreHelper::simplifyWeight( $value );
790 }
791 );
792
793 $priceField = $limit->addText( 'price', __( 'Price', 'packeta' ) . ':' );
794 $priceRules = $priceField->addConditionOn( $pricingTypeComponent, Form::EQUAL, Options::PRICING_TYPE_BY_WEIGHT );
795 $priceRules->setRequired();
796 $priceRules->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) );
797 $priceRules->addRule( Form::MIN, null, 0 );
798 // translators: %d is numeric threshold.
799 $weightRules->addRule( [ FormValidators::class, 'greaterThan' ], __( 'Enter number greater than %d', 'packeta' ), 0.0 );
800 }
801
802 /**
803 * @param Container $productValueLimits
804 * @param int|string $index
805 *
806 * @throws \RuntimeException
807 */
808 private function addProductValueLimit( Container $productValueLimits, $index ): void {
809 $form = $productValueLimits->getForm();
810 if ( $form === null ) {
811 throw new \RuntimeException( 'Form is not attached to the container.' );
812 }
813
814 $pricingTypeComponent = $form->getComponent( self::FORM_FIELD_PRICING_TYPE, false );
815
816 if ( ! $pricingTypeComponent instanceof Control ) {
817 throw new \RuntimeException(
818 sprintf(
819 'Component "%s" must be an instance of %s, %s given.',
820 self::FORM_FIELD_PRICING_TYPE,
821 Control::class,
822 is_object( $pricingTypeComponent ) ? get_class( $pricingTypeComponent ) : gettype( $pricingTypeComponent )
823 )
824 );
825 }
826
827 $limit = $productValueLimits->addContainer( $index );
828
829 $valueField = $limit->addText( 'value', __( 'Product value up to', 'packeta' ) . ':' );
830 $valueRules = $valueField->addConditionOn( $pricingTypeComponent, Form::EQUAL, Options::PRICING_TYPE_BY_PRODUCT_VALUE );
831 $valueRules->setRequired();
832 $valueRules->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) );
833 // translators: %d is numeric threshold.
834 $valueRules->addRule( Form::MIN, null, 0.0 );
835 // translators: %d is numeric threshold.
836 $valueRules->addRule( [ FormValidators::class, 'greaterThan' ], __( 'Enter number greater than %d', 'packeta' ), 0.0 );
837
838 $priceField = $limit->addText( 'price', __( 'Price', 'packeta' ) . ':' );
839 $priceRules = $priceField->addConditionOn( $pricingTypeComponent, Form::EQUAL, Options::PRICING_TYPE_BY_PRODUCT_VALUE );
840 $priceRules->setRequired();
841 $priceRules->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) );
842 // translators: %d is numeric threshold.
843 $priceRules->addRule( Form::MIN, null, 0.0 );
844 }
845
846 /**
847 * Adds limit fields to form.
848 *
849 * @param Container $surchargeLimits Container.
850 * @param int|string $index Index.
851 *
852 * @return void
853 */
854 private function addSurchargeLimit( Container $surchargeLimits, $index ): void {
855 $limit = $surchargeLimits->addContainer( (string) $index );
856 $item = $limit->addText( 'order_price', __( 'Order price up to', 'packeta' ) . ':' );
857 $item->setRequired();
858 $item->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) );
859 $item->addRule( Form::MIN, null, 0 );
860 $item->addCondition( Form::MAX, 0 )
861 ->addCondition( Form::MIN, 0 )
862 // translators: %d is the value.
863 ->addRule( Form::BLANK, __( 'Value must not be %d', 'packeta' ), 0 );
864
865 $item = $limit->addText( 'surcharge', __( 'Surcharge', 'packeta' ) . ':' );
866 $item->setRequired();
867 $item->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) );
868 $item->addRule( Form::MIN, null, 0 );
869 }
870
871 /**
872 * Creates link to page.
873 *
874 * @param string|null $countryCode Country code.
875 * @param string|null $carrierId Carrier ID.
876 *
877 * @return string
878 */
879 public function createUrl( ?string $countryCode = null, ?string $carrierId = null ): string {
880 $params = [
881 'page' => self::SLUG,
882 ];
883
884 if ( $countryCode !== null ) {
885 $params[ self::PARAMETER_COUNTRY_CODE ] = $countryCode;
886 }
887
888 if ( $carrierId !== null ) {
889 $params[ self::PARAMETER_CARRIER_ID ] = $carrierId;
890 }
891
892 return add_query_arg(
893 $params,
894 admin_url( 'admin.php' )
895 );
896 }
897
898 /**
899 * Gets checked vendors.
900 *
901 * @param array $options Form options.
902 *
903 * @return array
904 */
905 private function getCheckedVendors( array $options ): array {
906 $vendorCodes = [];
907 if ( isset( $options['vendor_groups'] ) ) {
908 $vendorCodes = $options['vendor_groups'];
909 }
910
911 $newVendors = [];
912 foreach ( $vendorCodes as $vendorId => $isChecked ) {
913 if ( ! $isChecked ) {
914 continue;
915 }
916 $newVendors[] = $vendorId;
917 }
918
919 return $newVendors;
920 }
921
922 /**
923 * Gets available vendors for compound carrier.
924 *
925 * @param string $id Compound carrier id.
926 *
927 * @return array|null
928 */
929 private function getAvailableVendors( $id ): ?array {
930 if ( ! $this->pickupPointsConfig->isCompoundCarrierId( $id ) ) {
931 return null;
932 }
933
934 $compoundCarriers = $this->pickupPointsConfig->getCompoundCarriers();
935 foreach ( $compoundCarriers as $compoundCarrier ) {
936 if ( $id === $compoundCarrier->getId() ) {
937 return $compoundCarrier->getVendorCodes();
938 }
939 }
940
941 return null;
942 }
943
944 /**
945 * Gets configuration of vendor checkboxes.
946 *
947 * @param string $carrierId Carrier id.
948 * @param array|null $carrierOptions Carrier options.
949 *
950 * @return array
951 */
952 private function getVendorCheckboxesConfig( string $carrierId, ?array $carrierOptions ): array {
953 $availableVendors = $this->getAvailableVendors( $carrierId );
954 if ( $availableVendors === null || $this->isAvailableVendorsCountLowerThanRequiredMinimum( $availableVendors ) ) {
955 return [];
956 }
957
958 $vendorCheckboxes = [];
959 $vendorCarriers = $this->pickupPointsConfig->getVendorCarriers();
960 foreach ( $availableVendors as $vendorId ) {
961 $vendorProvider = $vendorCarriers[ $vendorId ];
962 $checkbox = [
963 'group' => $vendorProvider->getGroup(),
964 'name' => $vendorProvider->getName(),
965 'disabled' => null,
966 'default' => null,
967 ];
968 $hasGroupSettingsSaved = isset( $carrierOptions['vendor_groups'] );
969 $hasTheGroupAllowed = (
970 $hasGroupSettingsSaved &&
971 in_array( $vendorProvider->getGroup(), $carrierOptions['vendor_groups'], true )
972 );
973 if ( ! $hasGroupSettingsSaved || $hasTheGroupAllowed ) {
974 $checkbox['default'] = true;
975 }
976 $vendorCheckboxes[] = $checkbox;
977 }
978
979 return $vendorCheckboxes;
980 }
981
982 /**
983 * Check if the number of vendors is lower than the required minimum
984 *
985 * @param array $availableVendors Available vendors.
986 *
987 * @return bool
988 */
989 public function isAvailableVendorsCountLowerThanRequiredMinimum( array $availableVendors ): bool {
990 return count( $availableVendors ) <= self::MINIMUM_CHECKED_VENDORS;
991 }
992
993 /**
994 * Checks if the number of vendors is lower than the minimum required by the carrier id
995 *
996 * @param string $carrierId Carrier id.
997 *
998 * @return bool
999 */
1000 public function isAvailableVendorsCountLowByCarrierId( string $carrierId ): bool {
1001 $availableVendors = $this->getAvailableVendors( $carrierId );
1002
1003 return is_array( $availableVendors ) && $this->isAvailableVendorsCountLowerThanRequiredMinimum( $availableVendors );
1004 }
1005 }
1006