PluginProbe
Packeta / 2.1
Packeta v2.1
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.1, at src/Packetery/Module/Carrier/OptionsPage.php

1,007 lines 35.5 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\Dashboard\DashboardPage;
17 use Packetery\Module\FormFactory;
18 use Packetery\Module\FormValidators;
19 use Packetery\Module\Framework\WpAdapter;
20 use Packetery\Module\MessageManager;
21 use Packetery\Module\ModuleHelper;
22 use Packetery\Module\Options\FlagManager\FeatureFlagProvider;
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 DashboardPage::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, true );
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 'carrierUnavailable' => $this->wpAdapter->__( 'This carrier is unavailable.', 'packeta' ),
691 ];
692 }
693
694 /**
695 * Transforms new_ keys to common numeric.
696 *
697 * @param array $options Options to merge.
698 * @param string $limitsContainer Container id.
699 *
700 * @return array
701 */
702 private function mergeNewLimits( array $options, string $limitsContainer ): array {
703 $newOptions = [];
704 if ( isset( $options[ $limitsContainer ] ) ) {
705 foreach ( $options[ $limitsContainer ] as $key => $option ) {
706 if ( is_int( $key ) ) {
707 $newOptions[ $key ] = $option;
708 }
709 if ( strpos( (string) $key, 'new_' ) === 0 ) {
710 $newOptions[] = $option;
711 }
712 }
713 $options[ $limitsContainer ] = $newOptions;
714 }
715
716 return $options;
717 }
718
719 /**
720 * Checks rules overlapping.
721 *
722 * @param Form $form Form.
723 * @param array $options Form data.
724 * @param string $limitsContainer Container id.
725 * @param string $limitKey Rule id.
726 * @param string $overlappingMessage Error message.
727 *
728 * @return void
729 */
730 private function checkOverlapping( Form $form, array $options, string $limitsContainer, string $limitKey, string $overlappingMessage ): void {
731 $limits = array_column( $options[ $limitsContainer ], $limitKey );
732 if ( count( array_unique( $limits, SORT_NUMERIC ) ) !== count( $limits ) ) {
733 add_settings_error( $limitsContainer, $limitsContainer, esc_attr( $overlappingMessage ) );
734 $form->addError( $overlappingMessage );
735 }
736 }
737
738 /**
739 * Sorts rules.
740 *
741 * @param array $options Form data.
742 * @param string $limitsContainer Container id.
743 * @param string $limitKey Rule id.
744 *
745 * @return array
746 */
747 private function sortLimits( array $options, string $limitsContainer, string $limitKey ): array {
748 $limits = array_column( $options[ $limitsContainer ], $limitKey );
749 array_multisort( $limits, SORT_ASC, $options[ $limitsContainer ] );
750
751 return $options;
752 }
753
754 /**
755 * @param Container $weightLimits
756 * @param int|string $index
757 *
758 * @throws \RuntimeException
759 */
760 private function addWeightLimit( Container $weightLimits, $index ): void {
761 $form = $weightLimits->getForm();
762 if ( $form === null ) {
763 throw new \RuntimeException( 'Form is not attached to the container.' );
764 }
765
766 $pricingTypeComponent = $form->getComponent( self::FORM_FIELD_PRICING_TYPE, false );
767
768 if ( ! $pricingTypeComponent instanceof Control ) {
769 throw new \RuntimeException(
770 sprintf(
771 'Component "%s" must be an instance of %s, %s given.',
772 self::FORM_FIELD_PRICING_TYPE,
773 Control::class,
774 is_object( $pricingTypeComponent ) ? get_class( $pricingTypeComponent ) : gettype( $pricingTypeComponent )
775 )
776 );
777 }
778
779 $limit = $weightLimits->addContainer( (string) $index );
780 $weightField = $limit->addText( 'weight', __( 'Weight up to', 'packeta' ) . ':' );
781 $weightRules = $weightField->addConditionOn( $pricingTypeComponent, Form::EQUAL, Options::PRICING_TYPE_BY_WEIGHT );
782 $weightRules->setRequired();
783 $weightRules->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) );
784 $weightRules->addRule( Form::MIN, null, 0 );
785 // translators: %d is numeric threshold.
786 $weightRules->addRule( [ FormValidators::class, 'greaterThan' ], __( 'Enter number greater than %d', 'packeta' ), 0.0 );
787
788 $weightRules->addFilter(
789 function ( float $value ) {
790 return CoreHelper::simplifyWeight( $value );
791 }
792 );
793
794 $priceField = $limit->addText( 'price', __( 'Price', 'packeta' ) . ':' );
795 $priceRules = $priceField->addConditionOn( $pricingTypeComponent, Form::EQUAL, Options::PRICING_TYPE_BY_WEIGHT );
796 $priceRules->setRequired();
797 $priceRules->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) );
798 $priceRules->addRule( Form::MIN, null, 0 );
799 // translators: %d is numeric threshold.
800 $weightRules->addRule( [ FormValidators::class, 'greaterThan' ], __( 'Enter number greater than %d', 'packeta' ), 0.0 );
801 }
802
803 /**
804 * @param Container $productValueLimits
805 * @param int|string $index
806 *
807 * @throws \RuntimeException
808 */
809 private function addProductValueLimit( Container $productValueLimits, $index ): void {
810 $form = $productValueLimits->getForm();
811 if ( $form === null ) {
812 throw new \RuntimeException( 'Form is not attached to the container.' );
813 }
814
815 $pricingTypeComponent = $form->getComponent( self::FORM_FIELD_PRICING_TYPE, false );
816
817 if ( ! $pricingTypeComponent instanceof Control ) {
818 throw new \RuntimeException(
819 sprintf(
820 'Component "%s" must be an instance of %s, %s given.',
821 self::FORM_FIELD_PRICING_TYPE,
822 Control::class,
823 is_object( $pricingTypeComponent ) ? get_class( $pricingTypeComponent ) : gettype( $pricingTypeComponent )
824 )
825 );
826 }
827
828 $limit = $productValueLimits->addContainer( $index );
829
830 $valueField = $limit->addText( 'value', __( 'Product value up to', 'packeta' ) . ':' );
831 $valueRules = $valueField->addConditionOn( $pricingTypeComponent, Form::EQUAL, Options::PRICING_TYPE_BY_PRODUCT_VALUE );
832 $valueRules->setRequired();
833 $valueRules->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) );
834 // translators: %d is numeric threshold.
835 $valueRules->addRule( Form::MIN, null, 0.0 );
836 // translators: %d is numeric threshold.
837 $valueRules->addRule( [ FormValidators::class, 'greaterThan' ], __( 'Enter number greater than %d', 'packeta' ), 0.0 );
838
839 $priceField = $limit->addText( 'price', __( 'Price', 'packeta' ) . ':' );
840 $priceRules = $priceField->addConditionOn( $pricingTypeComponent, Form::EQUAL, Options::PRICING_TYPE_BY_PRODUCT_VALUE );
841 $priceRules->setRequired();
842 $priceRules->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) );
843 // translators: %d is numeric threshold.
844 $priceRules->addRule( Form::MIN, null, 0.0 );
845 }
846
847 /**
848 * Adds limit fields to form.
849 *
850 * @param Container $surchargeLimits Container.
851 * @param int|string $index Index.
852 *
853 * @return void
854 */
855 private function addSurchargeLimit( Container $surchargeLimits, $index ): void {
856 $limit = $surchargeLimits->addContainer( (string) $index );
857 $item = $limit->addText( 'order_price', __( 'Order price up to', 'packeta' ) . ':' );
858 $item->setRequired();
859 $item->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) );
860 $item->addRule( Form::MIN, null, 0 );
861 $item->addCondition( Form::MAX, 0 )
862 ->addCondition( Form::MIN, 0 )
863 // translators: %d is the value.
864 ->addRule( Form::BLANK, __( 'Value must not be %d', 'packeta' ), 0 );
865
866 $item = $limit->addText( 'surcharge', __( 'Surcharge', 'packeta' ) . ':' );
867 $item->setRequired();
868 $item->addRule( Form::FLOAT, __( 'Please enter a valid decimal number.', 'packeta' ) );
869 $item->addRule( Form::MIN, null, 0 );
870 }
871
872 /**
873 * Creates link to page.
874 *
875 * @param string|null $countryCode Country code.
876 * @param string|null $carrierId Carrier ID.
877 *
878 * @return string
879 */
880 public function createUrl( ?string $countryCode = null, ?string $carrierId = null ): string {
881 $params = [
882 'page' => self::SLUG,
883 ];
884
885 if ( $countryCode !== null ) {
886 $params[ self::PARAMETER_COUNTRY_CODE ] = $countryCode;
887 }
888
889 if ( $carrierId !== null ) {
890 $params[ self::PARAMETER_CARRIER_ID ] = $carrierId;
891 }
892
893 return add_query_arg(
894 $params,
895 admin_url( 'admin.php' )
896 );
897 }
898
899 /**
900 * Gets checked vendors.
901 *
902 * @param array $options Form options.
903 *
904 * @return array
905 */
906 private function getCheckedVendors( array $options ): array {
907 $vendorCodes = [];
908 if ( isset( $options['vendor_groups'] ) ) {
909 $vendorCodes = $options['vendor_groups'];
910 }
911
912 $newVendors = [];
913 foreach ( $vendorCodes as $vendorId => $isChecked ) {
914 if ( ! $isChecked ) {
915 continue;
916 }
917 $newVendors[] = $vendorId;
918 }
919
920 return $newVendors;
921 }
922
923 /**
924 * Gets available vendors for compound carrier.
925 *
926 * @param string $id Compound carrier id.
927 *
928 * @return array|null
929 */
930 private function getAvailableVendors( $id ): ?array {
931 if ( ! $this->pickupPointsConfig->isCompoundCarrierId( $id ) ) {
932 return null;
933 }
934
935 $compoundCarriers = $this->pickupPointsConfig->getCompoundCarriers();
936 foreach ( $compoundCarriers as $compoundCarrier ) {
937 if ( $id === $compoundCarrier->getId() ) {
938 return $compoundCarrier->getVendorCodes();
939 }
940 }
941
942 return null;
943 }
944
945 /**
946 * Gets configuration of vendor checkboxes.
947 *
948 * @param string $carrierId Carrier id.
949 * @param array|null $carrierOptions Carrier options.
950 *
951 * @return array
952 */
953 private function getVendorCheckboxesConfig( string $carrierId, ?array $carrierOptions ): array {
954 $availableVendors = $this->getAvailableVendors( $carrierId );
955 if ( $availableVendors === null || $this->isAvailableVendorsCountLowerThanRequiredMinimum( $availableVendors ) ) {
956 return [];
957 }
958
959 $vendorCheckboxes = [];
960 $vendorCarriers = $this->pickupPointsConfig->getVendorCarriers();
961 foreach ( $availableVendors as $vendorId ) {
962 $vendorProvider = $vendorCarriers[ $vendorId ];
963 $checkbox = [
964 'group' => $vendorProvider->getGroup(),
965 'name' => $vendorProvider->getName(),
966 'disabled' => null,
967 'default' => null,
968 ];
969 $hasGroupSettingsSaved = isset( $carrierOptions['vendor_groups'] );
970 $hasTheGroupAllowed = (
971 $hasGroupSettingsSaved &&
972 in_array( $vendorProvider->getGroup(), $carrierOptions['vendor_groups'], true )
973 );
974 if ( ! $hasGroupSettingsSaved || $hasTheGroupAllowed ) {
975 $checkbox['default'] = true;
976 }
977 $vendorCheckboxes[] = $checkbox;
978 }
979
980 return $vendorCheckboxes;
981 }
982
983 /**
984 * Check if the number of vendors is lower than the required minimum
985 *
986 * @param array $availableVendors Available vendors.
987 *
988 * @return bool
989 */
990 public function isAvailableVendorsCountLowerThanRequiredMinimum( array $availableVendors ): bool {
991 return count( $availableVendors ) <= self::MINIMUM_CHECKED_VENDORS;
992 }
993
994 /**
995 * Checks if the number of vendors is lower than the minimum required by the carrier id
996 *
997 * @param string $carrierId Carrier id.
998 *
999 * @return bool
1000 */
1001 public function isAvailableVendorsCountLowByCarrierId( string $carrierId ): bool {
1002 $availableVendors = $this->getAvailableVendors( $carrierId );
1003
1004 return is_array( $availableVendors ) && $this->isAvailableVendorsCountLowerThanRequiredMinimum( $availableVendors );
1005 }
1006 }
1007