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

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