PluginProbe
PostNL for WooCommerce / 5.9.12
PostNL for WooCommerce v5.9.12
5.9.12 5.9.11 5.9.10 5.9.9 5.9.8 5.9.7 5.9.6 trunk 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 3.1.4 3.1.5 3.1.6 3.1.7 4.0.0 4.0.1 4.0.2 4.3.2 4.3.3 4.4.0 4.4.1 All 72 releases
woo-postnl / src / Shipping_Method / PostNL.php

PostNL.php in PostNL for WooCommerce 5.9.12, at src/Shipping_Method/PostNL.php

425 lines 14.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Shipping_Method/PostNL file.
4 *
5 * @package PostNLWooCommerce\Shipping_Method
6 */
7
8 namespace PostNLWooCommerce\Shipping_Method;
9
10 use PostNLWooCommerce\Rest_API\Barcode\Key_Validator;
11 use PostNLWooCommerce\Utils;
12 use WC_Admin_Settings;
13
14 if ( ! defined( 'ABSPATH' ) ) {
15 exit;
16 }
17
18 /**
19 * Class PostNL
20 *
21 * @package PostNLWooCommerce\Shipping_Method
22 */
23 class PostNL extends \WC_Shipping_Flat_Rate {
24 /**
25 * Merchant codes option name.
26 */
27 const MERCHANT_CODES_OPTION = 'postnl_merchant_codes';
28
29 /**
30 * Init and hook in the integration.
31 *
32 * @param int $instance_id Instance ID.
33 */
34 public function __construct( $instance_id = 0 ) {
35 $this->id = POSTNL_SETTINGS_ID;
36 $this->instance_id = absint( $instance_id );
37 $this->method_title = POSTNL_SERVICE_NAME;
38
39 // translators: %1$s & %2$s is replaced with <a> tag.
40 $this->method_description = sprintf( __( 'Below you will find all functions for controlling, preparing and processing your shipment with PostNL. Prerequisite is a valid PostNL business customer contract. If you are not yet a PostNL business customer, you can request a quote %1$shere%2$s.', 'postnl-for-woocommerce' ), '<a href="https://mijnpostnlzakelijk.postnl.nl/s/become-a-customer?language=nl_NL#/" target="_blank">', '</a>' );
41 $this->supports = array(
42 'shipping-zones',
43 'instance-settings',
44 'instance-settings-modal',
45 'settings',
46 );
47
48 $this->postnl_init();
49 }
50
51 /**
52 * Shipping method initialization.
53 */
54 public function postnl_init() {
55 $this->init();
56 $this->init_form_fields();
57 $this->init_settings();
58
59 add_filter( 'woocommerce_shipping_instance_form_fields_' . $this->id, array( $this, 'instance_form_fields' ), 10, 1 );
60 add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
61 add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_shipping_method_assets' ) );
62 }
63
64 /**
65 * Process admin options.
66 *
67 * @return void
68 */
69 public function process_admin_options() {
70 parent::process_admin_options();
71
72 // parent::process_admin_options() writes the new values to the DB but only
73 // updates this method object's cache; the Settings singleton still holds
74 // the pre-save values, and the status row, the NewKey header and the
75 // effective-key selection all read through it for the rest of the request.
76 // Refresh it so the screen that comes back reflects what was just saved.
77 Settings::get_instance()->init_settings();
78
79 $this->process_merchant_codes();
80
81 // When pickup is disabled, reset the default checkout tab to its default value.
82 if ( 'yes' !== $this->get_option( 'enable_pickup_points' ) ) {
83 $this->update_option( 'default_checkout_tab', 'delivery_day' );
84 }
85
86 $this->process_new_api_key_validation();
87 }
88
89 /**
90 * Validate the "New API Key" field whenever settings are saved.
91 *
92 * Runs in both environments so the key can be checked in sandbox as well as
93 * production. Performs a live Barcode API call with the candidate key for the
94 * active environment. Success flips the validated flag on so the plugin starts
95 * routing traffic through the new key; failure leaves the old key in use,
96 * records why, and surfaces the reason to the merchant.
97 */
98 protected function process_new_api_key_validation() {
99 $settings = Settings::get_instance();
100
101 // The environment is read from the freshly-posted value on $this rather
102 // than the settings singleton, which may still hold the pre-save value.
103 $is_sandbox = ( 'production' !== $this->get_option( 'environment_mode' ) );
104 $new_field = $is_sandbox ? 'api_keys_sandbox_new' : 'api_keys_new';
105 $orig_field = $is_sandbox ? 'api_keys_sandbox' : 'api_keys';
106
107 $new_key = trim( (string) $this->get_option( $new_field ) );
108 $original = trim( (string) $this->get_option( $orig_field ) );
109
110 if ( '' === $new_key || $new_key === $original ) {
111 $settings->set_api_key_new_validated( false, null, $is_sandbox );
112 $settings->set_new_key_status_reason( '', $is_sandbox );
113 return;
114 }
115
116 // This exact key already passed validation; skip the live call so an
117 // unrelated settings save doesn't make a blocking request every time.
118 if ( $settings->is_api_key_new_validated_value( $new_key, $is_sandbox ) ) {
119 return;
120 }
121
122 $result = Key_Validator::validate(
123 $new_key,
124 $this->get_option( 'customer_code' ),
125 $this->get_option( 'customer_num' ),
126 $is_sandbox
127 );
128
129 if ( true === $result ) {
130 $settings->set_api_key_new_validated( true, $new_key, $is_sandbox );
131 $settings->set_new_key_status_reason( '', $is_sandbox );
132 return;
133 }
134
135 $reason = $result->get_error_code();
136
137 // "unreachable" means PostNL never returned a verdict, so we cannot say the
138 // key is bad — leave any previously-validated state as it was rather than
139 // showing a false red. Every other reason is a definite "not usable yet".
140 if ( Key_Validator::REASON_UNREACHABLE !== $reason ) {
141 $settings->set_api_key_new_validated( false, null, $is_sandbox );
142 }
143
144 $settings->set_new_key_status_reason( $reason, $is_sandbox );
145 WC_Admin_Settings::add_error( esc_html( Key_Validator::reason_message( $reason ) ) );
146 }
147
148 /**
149 * Render the status row shown beneath the API Key field: a coloured label,
150 * an em dash and a plain sentence describing the new key's state, driven by
151 * the same value sent in the NewKey header. Hidden by the settings JS for
152 * whichever environment is not currently selected.
153 *
154 * @param string $key Field key.
155 * @param array $data Field data (expects an 'environment' of 'sandbox' or 'production').
156 *
157 * @return string
158 */
159 public function generate_postnl_new_key_status_html( $key, $data ) {
160 $is_sandbox = ( isset( $data['environment'] ) && 'sandbox' === $data['environment'] );
161 $status = Settings::get_instance()->get_new_key_status( $is_sandbox );
162
163 ob_start();
164 ?>
165 <tr valign="top" class="postnl-new-key-status-row" data-postnl-env="<?php echo esc_attr( $is_sandbox ? 'sandbox' : 'production' ); ?>">
166 <th scope="row" class="titledesc"><?php esc_html_e( 'API key status', 'postnl-for-woocommerce' ); ?></th>
167 <td class="forminp">
168 <p style="margin-top:0;">
169 <strong class="postnl-new-key-status-label" style="color:<?php echo esc_attr( $status['color'] ); ?>;"><?php echo esc_html( $status['label'] ); ?></strong>
170 &mdash; <span class="postnl-new-key-status-summary"><?php echo esc_html( $status['summary'] ); ?></span>
171 </p>
172 <p class="description postnl-new-key-status-desc" style="margin-top:4px;<?php echo empty( $status['description'] ) ? 'display:none;' : ''; ?>"><?php echo wp_kses_post( $status['description'] ); ?></p>
173 </td>
174 </tr>
175 <?php
176 return ob_get_clean();
177 }
178
179 /**
180 * Calculate the shipping costs.
181 *
182 * @param array $package Package of items from cart.
183 */
184 public function calculate_shipping( $package = array() ) {
185 // Determine whether the cart subtotal exceeds the free-shipping threshold.
186 $minimum_for_free_shipping = $this->get_option( 'minimum_for_free_shipping' );
187 $is_free = '' !== $minimum_for_free_shipping && $package['cart_subtotal'] > $minimum_for_free_shipping;
188
189 if ( $is_free ) {
190 $rate = array(
191 'id' => $this->get_rate_id(),
192 'label' => $this->title,
193 'cost' => 0,
194 'package' => $package,
195 );
196 $this->add_rate( $rate );
197 } else {
198 parent::calculate_shipping( $package );
199 }
200 }
201
202 /**
203 * Add form fields for PostNL.
204 *
205 * @param Array $form_fields List of instance form fields.
206 *
207 * @return Array
208 */
209 public function instance_form_fields( $form_fields ) {
210 // Change title default value.
211 $form_fields['title']['default'] = $this->method_title;
212
213 // Minimum for free shipping.
214 $currency_symbol = get_woocommerce_currency_symbol();
215
216 $form_fields['minimum_for_free_shipping'] = array(
217 // Translators: %s is the currency symbol.
218 'title' => sprintf( esc_html__( 'Free shipping from %s', 'postnl-for-woocommerce' ), $currency_symbol ),
219 'type' => 'number',
220 'desc_tip' => esc_html__( 'Keep empty if you don’t want to use Free shipping', 'postnl-for-woocommerce' ),
221 'default' => 0,
222 );
223
224 return $form_fields;
225 }
226
227 /**
228 * Initialize integration settings form fields.
229 *
230 * @return void
231 */
232 public function init_form_fields() {
233 $base_country = Utils::get_base_country();
234 $settings = Settings::get_instance();
235
236 if ( 'NL' === $base_country ) {
237 $form_fields = $settings->nl_setting_fields();
238 } elseif ( 'BE' === $base_country ) {
239 $form_fields = $settings->be_setting_fields();
240 } else {
241 $form_fields = $settings->filter_setting_fields( '' );
242 }
243
244 $this->form_fields = $form_fields;
245 }
246
247 /**
248 * Enqueue js file in shipping method settings page.
249 */
250 public function enqueue_shipping_method_assets() {
251 $screen = get_current_screen();
252 if ( ! empty( $screen->id ) && 'woocommerce_page_wc-settings' === $screen->id && ! empty( $_GET['section'] ) && POSTNL_SETTINGS_ID === wp_unslash( $_GET['section'] ) ) {
253 wp_enqueue_script(
254 'postnl-admin-settings',
255 POSTNL_WC_PLUGIN_DIR_URL . '/assets/js/admin-settings.js',
256 array( 'jquery' ),
257 POSTNL_WC_VERSION,
258 true
259 );
260
261 wp_localize_script(
262 'postnl-admin-settings',
263 'postnlApiKeyCheck',
264 array(
265 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
266 'action' => \PostNLWooCommerce\Admin\Api_Key_Check::AJAX_ACTION,
267 'nonce' => wp_create_nonce( \PostNLWooCommerce\Admin\Api_Key_Check::NONCE_ACTION ),
268 )
269 );
270 }
271 }
272
273 /**
274 * Generate repeater HTML.
275 *
276 * @param string $key Field key.
277 * @param array $data Field data.
278 *
279 * @return string
280 */
281 public function generate_repeater_html( $key, $data ) {
282 ob_start();
283 $merchant_codes = get_option( self::MERCHANT_CODES_OPTION, array() );
284 $non_eu_countries = Utils::get_non_eu_countries();
285
286 ?>
287 <tr valign="top">
288 <th scope="row" class="titledesc">
289 <label for="<?php echo esc_attr( $data['id'] ?? $key ); ?>"><?php echo wp_kses_post( $data['title'] ?? '' ); ?></label>
290 <?php echo $this->get_tooltip_html( $data ); ?>
291 </th>
292 <td class="forminp">
293 <div id="postnl-merchant-codes-repeater">
294 <div class="merchant-codes-header">
295 <div class="merchant-codes-row merchant-codes-header-row">
296 <div class="country-column">
297 <strong><?php esc_html_e( 'Country', 'postnl-for-woocommerce' ); ?></strong>
298 </div>
299 <div class="code-column">
300 <strong><?php esc_html_e( 'Merchant Code', 'postnl-for-woocommerce' ); ?></strong>
301 </div>
302 <div class="action-column">
303 <strong><?php esc_html_e( 'Action', 'postnl-for-woocommerce' ); ?></strong>
304 </div>
305 </div>
306 </div>
307
308 <div class="merchant-codes-rows" id="merchant-codes-rows">
309 <?php if ( ! empty( $merchant_codes ) ) : ?>
310 <?php foreach ( $merchant_codes as $country_code => $merchant_code ) : ?>
311 <div class="merchant-codes-row">
312 <div class="country-column">
313 <select name="<?php echo esc_attr( self::MERCHANT_CODES_OPTION ); ?>_countries[]" class="country-select">
314 <option value=""><?php esc_html_e( 'Select Country', 'postnl-for-woocommerce' ); ?></option>
315 <?php foreach ( $non_eu_countries as $code => $name ) : ?>
316 <option value="<?php echo esc_attr( $code ); ?>" <?php selected( $country_code, $code ); ?>>
317 <?php echo esc_html( $name ); ?> (<?php echo esc_html( $code ); ?>)
318 </option>
319 <?php endforeach; ?>
320 </select>
321 </div>
322 <div class="code-column">
323 <input type="text"
324 name="<?php echo esc_attr( self::MERCHANT_CODES_OPTION ); ?>_codes[]"
325 value="<?php echo esc_attr( $merchant_code ); ?>"
326 placeholder="<?php esc_attr_e( 'Enter merchant code', 'postnl-for-woocommerce' ); ?>"
327 class="regular-text"
328 />
329 </div>
330 <div class="action-column">
331 <button type="button" class="button remove-row"><?php esc_html_e( 'Remove', 'postnl-for-woocommerce' ); ?></button>
332 </div>
333 </div>
334 <?php endforeach; ?>
335 <?php endif; ?>
336 </div>
337
338 <div class="merchant-codes-actions">
339 <button type="button" class="button button-secondary" id="add-merchant-code-row">
340 <?php esc_html_e( 'Add Merchant Code', 'postnl-for-woocommerce' ); ?>
341 </button>
342 </div>
343 </div>
344
345 <?php if ( isset( $data['description'] ) && ! empty( $data['description'] ) ) : ?>
346 <p class="description"><?php echo wp_kses_post( $data['description'] ); ?></p>
347 <?php endif; ?>
348
349 <!-- Row template for JavaScript -->
350 <script type="text/template" id="merchant-code-row-template">
351 <div class="merchant-codes-row">
352 <div class="country-column">
353 <select name="<?php echo esc_attr( self::MERCHANT_CODES_OPTION ); ?>_countries[]" class="country-select">
354 <option value=""><?php esc_html_e( 'Select Country', 'postnl-for-woocommerce' ); ?></option>
355 <?php foreach ( $non_eu_countries as $code => $name ) : ?>
356 <option value="<?php echo esc_attr( $code ); ?>">
357 <?php echo esc_html( $name ); ?> (<?php echo esc_html( $code ); ?>)
358 </option>
359 <?php endforeach; ?>
360 </select>
361 </div>
362 <div class="code-column">
363 <input type="text"
364 name="<?php echo esc_attr( self::MERCHANT_CODES_OPTION ); ?>_codes[]"
365 value=""
366 placeholder="<?php esc_attr_e( 'Enter merchant code', 'postnl-for-woocommerce' ); ?>"
367 class="regular-text"
368 />
369 </div>
370 <div class="action-column">
371 <button type="button" class="button remove-row"><?php esc_html_e( 'Remove', 'postnl-for-woocommerce' ); ?></button>
372 </div>
373 </div>
374 </script>
375 </td>
376 </tr>
377 <?php
378 return ob_get_clean();
379 }
380
381 /**
382 * Save merchant codes from repeater.
383 */
384 public function process_merchant_codes() {
385 $merchant_codes = array();
386 $countries_key = self::MERCHANT_CODES_OPTION . '_countries';
387 $codes_key = self::MERCHANT_CODES_OPTION . '_codes';
388 $error = false;
389
390 if ( ! isset( $_POST[ $countries_key ] ) && ! isset( $_POST[ $codes_key ] ) ) {
391 update_option( self::MERCHANT_CODES_OPTION, array() );
392
393 return;
394 }
395
396 $countries = $_POST[ $countries_key ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
397 $codes = $_POST[ $codes_key ]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
398
399 // Check for duplicates in countries array.
400 if ( count( $countries ) !== count( array_unique( $countries ) ) ) {
401 WC_Admin_Settings::add_error(
402 esc_html__( 'Duplicate countries found and have been removed. Only the last entry for each country will be saved.', 'postnl-for-woocommerce' )
403 );
404 }
405
406 foreach ( $countries as $index => $country ) {
407 $code = $codes[ $index ] ?? null;
408
409 // Skip empty values or missing codes.
410 if ( empty( $country ) || empty( $code ) ) {
411 $error = true;
412 continue;
413 }
414
415 $merchant_codes[ sanitize_text_field( $country ) ] = sanitize_text_field( $code );
416 }
417
418 update_option( self::MERCHANT_CODES_OPTION, $merchant_codes );
419
420 if ( $error ) {
421 WC_Admin_Settings::add_error( esc_html__( 'Some merchant codes were not saved because of missing country or code.', 'postnl-for-woocommerce' ) );
422 }
423 }
424 }
425