PluginProbe
seQura / 4.0.0
seQura v4.0.0
4.3.4 4.3.3 4.3.2 4.3.1 trunk 2.0.0 2.0.10 2.0.11 2.0.12 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 3.0.0 3.0.2 3.0.5 3.0.6 3.0.7 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 4.0.0 All 30 releases
sequra / src / Repositories / Migrations / class-migration-install-400.php

class-migration-install-400.php in seQura 4.0.0, at src/Repositories/Migrations/class-migration-install-400.php

645 lines 21.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Post install migration for version 4.0.0 of the plugin.
4 *
5 * @package SeQura/WC
6 * @subpackage SeQura/WC/Repositories
7 */
8
9 namespace SeQura\WC\Repositories\Migrations;
10
11 use Exception;
12 use SeQura\Core\BusinessLogic\AdminAPI\AdminAPI;
13 use SeQura\Core\BusinessLogic\AdminAPI\Connection\Requests\ConnectionRequest;
14 use SeQura\Core\BusinessLogic\AdminAPI\Connection\Requests\OnboardingRequest;
15 use SeQura\Core\BusinessLogic\AdminAPI\CountryConfiguration\Requests\CountryConfigurationRequest;
16 use SeQura\Core\BusinessLogic\AdminAPI\GeneralSettings\Requests\GeneralSettingsRequest;
17 use SeQura\Core\BusinessLogic\AdminAPI\PromotionalWidgets\Requests\WidgetSettingsRequest;
18 use SeQura\Core\BusinessLogic\CheckoutAPI\CheckoutAPI;
19 use SeQura\Core\BusinessLogic\CheckoutAPI\PaymentMethods\Requests\GetCachedPaymentMethodsRequest;
20 use SeQura\Core\BusinessLogic\DataAccess\GeneralSettings\Entities\GeneralSettings;
21 use SeQura\Core\BusinessLogic\DataAccess\PromotionalWidgets\Entities\WidgetSettings;
22 use SeQura\Core\BusinessLogic\Domain\Multistore\StoreContext;
23 use SeQura\Core\BusinessLogic\Domain\PromotionalWidgets\Services\WidgetSettingsService;
24 use SeQura\Core\BusinessLogic\Utility\EncryptorInterface;
25 use Throwable;
26
27 /**
28 * Post install migration for version 4.0.0 of the plugin.
29 */
30 class Migration_Install_400 extends Migration {
31
32 /**
33 * Entity table.
34 *
35 * @var string
36 */
37 private $entity_table;
38
39 /**
40 * Encryptor service.
41 *
42 * @var EncryptorInterface
43 */
44 private $encryptor;
45
46 /**
47 * In-memory cache of payment methods.
48 *
49 * @var array<int, array{
50 * product: string,
51 * category: string,
52 * }>
53 */
54 private $payment_methods;
55
56 /**
57 * Store context
58 *
59 * @var StoreContext
60 */
61 private $store_context;
62
63 /**
64 * Get the plugin version when the changes were made.
65 */
66 public function get_version(): string {
67 return '4.0.0';
68 }
69
70 /**
71 * Constructor
72 *
73 * @param \wpdb $wpdb Database instance.
74 */
75 public function __construct(
76 \wpdb $wpdb,
77 EncryptorInterface $encryptor,
78 StoreContext $store_context
79 ) {
80 parent::__construct( $wpdb );
81 $this->entity_table = $this->db->prefix . 'sequra_entity';
82 $this->encryptor = $encryptor;
83 $this->store_context = $store_context;
84 }
85
86 /**
87 * Run the migration.
88 *
89 * @throws Throwable|Critical_Migration_Exception
90 */
91 public function run(): void {
92 $this->fetch_deployments();
93 $this->migrate_connection_data();
94 $this->migrate_general_settings();
95 $this->migrate_country_configuration();
96 $this->migrate_payment_methods();
97 $this->migrate_widget_settings();
98 }
99
100 /**
101 * Fetch deployment targets and save them to the database
102 *
103 * @throws Exception
104 */
105 private function fetch_deployments(): void {
106 $response = AdminAPI::get()
107 ->deployments( $this->store_context->getStoreId() )
108 ->getAllDeployments();
109
110 if ( ! $response->isSuccessful() ) {
111 throw new Exception( 'Error fetching deployment targets' );
112 }
113 }
114
115 /**
116 * Migrate connection data to the new schema.
117 *
118 * @throws Throwable|Exception
119 */
120 private function migrate_connection_data(): void {
121 $connections = $this->get_connection_data();
122
123 if ( empty( $connections ) ) {
124 // No connection data found, nothing to migrate.
125 return;
126 }
127
128 // Remove old connection data.
129 foreach ( $connections as $conn ) {
130 $this->db->delete( $this->entity_table, array( 'id' => $conn['id'] ) );
131 }
132 // Remove also old credentials data to avoid issues if the authentication fails for some deployments.
133 $this->db->delete( $this->entity_table, array( 'type' => 'Credentials' ) );
134
135 // Take the first connection only.
136 $conn = $connections[0];
137
138 $environment = $conn['connectionData']['environment'];
139 $username = $conn['connectionData']['authorizationCredentials']['username'];
140 $password = $conn['connectionData']['authorizationCredentials']['password'];
141
142 $response = AdminAPI::get()
143 ->connection( $this->store_context->getStoreId() )
144 ->connect(
145 new OnboardingRequest(
146 array(
147 new ConnectionRequest(
148 $environment,
149 '',
150 $username,
151 $password,
152 'sequra'
153 ),
154 new ConnectionRequest(
155 $environment,
156 '',
157 $username,
158 $password,
159 'svea'
160 ),
161 ),
162 true
163 )
164 );
165
166 if ( ! $response->isSuccessful() ) {
167 throw new Exception( 'Error migrating ConnectionData' );
168 }
169 }
170
171 /**
172 * Migrate country configuration to the new schema.
173 *
174 * @throws Throwable|Exception
175 */
176 private function migrate_country_configuration(): void {
177 // @phpstan-ignore-next-line
178 $query = $this->db->prepare( 'SELECT `index_2`, `index_3` FROM ' . $this->entity_table . ' WHERE `type` = %s AND `index_1` = %s', 'Credentials', $this->store_context->getStoreId() );
179 $rows = $this->db->get_results( $query, ARRAY_A );
180 if ( ! is_array( $rows ) ) {
181 return;
182 }
183
184 // Remove old connection data.
185 $this->db->delete( $this->entity_table, array( 'type' => 'CountryConfiguration' ) );
186
187 $country_configurations = array();
188 foreach ( $rows as $row ) {
189 if ( ! isset( $row['index_2'], $row['index_3'] )
190 || ! is_string( $row['index_3'] )
191 || '' === $row['index_3']
192 || ! is_string( $row['index_2'] )
193 || '' === $row['index_2'] ) {
194 continue;
195 }
196 $country_configurations[] = array(
197 'countryCode' => $row['index_2'],
198 'merchantId' => $row['index_3'],
199 );
200 }
201
202 $response = AdminAPI::get()
203 ->countryConfiguration( $this->store_context->getStoreId() )
204 ->saveCountryConfigurations(
205 new CountryConfigurationRequest( $country_configurations )
206 );
207
208 if ( ! $response->isSuccessful() ) {
209 throw new Exception( 'Error migrating CountryConfiguration' );
210 }
211 }
212
213 /**
214 * Migrate widget settings to the new schema.
215 *
216 * @throws Throwable|Exception
217 */
218 private function migrate_widget_settings(): void {
219
220 $data = $this->get_widget_settings();
221 if ( null === $data ) {
222 // No widget settings found, nothing to migrate.
223 return;
224 }
225
226 // Remove old widget settings to prevent errors due to non-existing classes.
227 if ( ! $this->db->delete( $this->entity_table, array( 'id' => $data['id'] ) ) ) {
228 throw new Exception( 'Error removing old WidgetSettings' );
229 }
230
231 $response = AdminAPI::get()
232 ->widgetConfiguration( $this->store_context->getStoreId() )
233 ->setWidgetSettings(
234 new WidgetSettingsRequest(
235 $data['widgetSettings']['displayOnProductPage'],
236 $data['widgetSettings']['showInstallmentsInProductListing'],
237 $data['widgetSettings']['showInstallmentsInCartPage'],
238 $data['widgetSettings']['widgetConfiguration'],
239 $data['widgetSettings']['widgetSettingsForProduct']['priceSelector'],
240 $data['widgetSettings']['widgetSettingsForProduct']['locationSelector'],
241 $data['widgetSettings']['widgetSettingsForCart']['priceSelector'],
242 $data['widgetSettings']['widgetSettingsForCart']['locationSelector'],
243 $data['widgetSettings']['widgetSettingsForCart']['widgetProduct'],
244 $data['widgetSettings']['widgetSettingsForListing']['widgetProduct'],
245 $data['widgetSettings']['widgetSettingsForListing']['priceSelector'],
246 $data['widgetSettings']['widgetSettingsForListing']['locationSelector'],
247 $data['widgetSettings']['widgetSettingsForProduct']['altPriceSelector'],
248 $data['widgetSettings']['widgetSettingsForProduct']['altPriceTriggerSelector'],
249 array_map(
250 function ( $item ) {
251 return array(
252 'selForTarget' => $item['customLocationSelector'],
253 'product' => $item['product'],
254 'displayWidget' => $item['displayWidget'],
255 'widgetStyles' => $item['customWidgetStyle'],
256 );
257 },
258 $data['widgetSettings']['widgetSettingsForProduct']['customWidgetSettings']
259 )
260 )
261 );
262
263 if ( ! $response->isSuccessful() ) {
264 throw new Exception( 'Error migrating WidgetSettings' );
265 }
266 }
267 /**
268 * Migrate GeneralSettings to the new schema.
269 *
270 * @throws Throwable|Exception
271 */
272 private function migrate_general_settings(): void {
273
274 $data = $this->get_general_settings();
275 if ( null === $data ) {
276 // No widget settings found, nothing to migrate.
277 return;
278 }
279
280 // Remove old GeneralSettings to prevent errors due to non-existing classes.
281 if ( ! $this->db->delete( $this->entity_table, array( 'id' => $data['id'] ) ) ) {
282 throw new Exception( 'Error removing old GeneralSettings' );
283 }
284
285 $response = AdminAPI::get()
286 ->generalSettings( $this->store_context->getStoreId() )
287 ->saveGeneralSettings(
288 new GeneralSettingsRequest(
289 $data['generalSettings']['sendOrderReportsPeriodicallyToSeQura'],
290 $data['generalSettings']['showSeQuraCheckoutAsHostedPage'],
291 $data['generalSettings']['allowedIPAddresses'],
292 $data['generalSettings']['excludedProducts'],
293 $data['generalSettings']['excludedCategories'],
294 $data['generalSettings']['defaultServicesEndDate']
295 )
296 );
297
298 if ( ! $response->isSuccessful() ) {
299 throw new Exception( 'Error migrating GeneralSettings' );
300 }
301 }
302
303 /**
304 * Migrate payment methods to the new schema.
305 *
306 * @throws Throwable|Exception
307 */
308 private function migrate_payment_methods(): void {
309 // Remove old payment methods if any.
310 $this->db->delete( $this->entity_table, array( 'type' => 'PaymentMethods' ) );
311 $this->db->delete( $this->entity_table, array( 'type' => 'PaymentMethod' ) );
312
313 // Force the caching of payment methods for every merchant.
314 foreach ( $this->get_merchant_ids() as $merchant_id ) {
315 $response = CheckoutAPI::get()
316 ->cachedPaymentMethods( $this->store_context->getStoreId() )
317 ->getCachedPaymentMethods(
318 new GetCachedPaymentMethodsRequest( $merchant_id )
319 );
320 if ( ! $response->isSuccessful() ) {
321 throw new Exception( \esc_html( 'Error migrating PaymentMethods for merchant ' . $merchant_id ) );
322 }
323 }
324 }
325
326 /**
327 * Get ConnectionData from the database.
328 *
329 * @return array<int,array{
330 * id: int,
331 * connectionData: array{
332 * environment: string,
333 * authorizationCredentials: array{
334 * username: string,
335 * password: string
336 * }
337 * }
338 * }>|null Connection data or null if not found.
339 */
340 private function get_connection_data(): ?array {
341 // @phpstan-ignore-next-line
342 $query = $this->db->prepare( 'SELECT `id`, `data` FROM ' . $this->entity_table . ' WHERE `type` = %s AND `index_1` = %s', 'ConnectionData', $this->store_context->getStoreId() );
343 $rows = $this->db->get_results( $query, ARRAY_A );
344 if ( ! is_array( $rows ) ) {
345 return null;
346 }
347 $data_arr = array();
348
349 foreach ( $rows as $row ) {
350 /**
351 * Data
352 *
353 * @var array{
354 * connectionData: array{
355 * environment: string,
356 * authorizationCredentials: array{
357 * username: string,
358 * password: string
359 * }
360 * }
361 * }|null $data
362 */
363 $data = isset( $row['data'] ) && is_string( $row['data'] ) ? json_decode( $row['data'], true ) : null;
364 if (
365 isset(
366 $row['id'],
367 $data['connectionData']['environment'],
368 $data['connectionData']['authorizationCredentials']['username'],
369 $data['connectionData']['authorizationCredentials']['password']
370 )
371 && is_string( $data['connectionData']['environment'] )
372 && is_string( $data['connectionData']['authorizationCredentials']['password'] )
373 && is_string( $data['connectionData']['authorizationCredentials']['username'] )
374 ) {
375 $data['id'] = (int) $row['id'];
376 $data['connectionData']['authorizationCredentials']['password'] = $this->encryptor->decrypt( $data['connectionData']['authorizationCredentials']['password'] );
377 $data_arr[] = $data;
378 }
379 }
380 return $data_arr;
381 }
382
383 /**
384 * Get an array of merchant ids from the database.
385 *
386 * @return array<string> Merchant ids.
387 */
388 private function get_merchant_ids(): array {
389 // @phpstan-ignore-next-line
390 $query = $this->db->prepare( 'SELECT DISTINCT `index_3` FROM ' . $this->entity_table . ' WHERE `type` = %s', 'Credentials' );
391 $rows = $this->db->get_results( $query, ARRAY_A );
392 if ( ! is_array( $rows ) ) {
393 return array();
394 }
395 $ids = array();
396 foreach ( $rows as $row ) {
397 if ( isset( $row['index_3'] ) && is_string( $row['index_3'] ) && '' !== $row['index_3'] ) {
398 $ids[] = $row['index_3'];
399 }
400 }
401 return $ids;
402 }
403
404 /**
405 * Get WidgetSettings from the database.
406 *
407 * @return array{
408 * id: int,
409 * widgetSettings: array{
410 * displayOnProductPage: bool,
411 * showInstallmentsInProductListing: bool,
412 * showInstallmentsInCartPage: bool,
413 * widgetConfiguration: string,
414 * widgetSettingsForProduct: array{
415 * priceSelector: string,
416 * locationSelector: string,
417 * altPriceSelector: string,
418 * altPriceTriggerSelector: string,
419 * customWidgetSettings: array<array{
420 * customLocationSelector: string,
421 * product: string,
422 * displayWidget: bool,
423 * customWidgetStyle: string
424 * }>
425 * },
426 * widgetSettingsForCart: array{
427 * priceSelector: string,
428 * locationSelector: string,
429 * widgetProduct: string
430 * },
431 * widgetSettingsForListing: array{
432 * priceSelector: string,
433 * locationSelector: string,
434 * widgetProduct: string
435 * }
436 * }
437 * }|null WidgetSettings or null if not found.
438 */
439 private function get_widget_settings(): ?array {
440 // @phpstan-ignore-next-line
441 $query = $this->db->prepare( 'SELECT `id`, `data` FROM ' . $this->entity_table . ' WHERE `type` = %s AND `index_1` = %s LIMIT 1', 'WidgetSettings', $this->store_context->getStoreId() );
442 $row = $this->db->get_row( $query, ARRAY_A );
443 $data = isset( $row['data'] ) && is_string( $row['data'] ) ? json_decode( $row['data'], true ) : null;
444
445 if ( ! is_array( $data ) || ! isset( $row['id'] ) ) {
446 return null;
447 }
448
449 if ( WidgetSettings::class === strval( $data['class_name'] ?? '' ) ) {
450 // Data is already in the new format, nothing to migrate.
451 return null;
452 }
453
454 $custom_widget_settings = array();
455 $source = $data['widgetSettings']['widgetSettingsForProduct']['customWidgetSettings'] ?? ( $data['widgetSettings']['widgetLocationConfiguration']['customLocations'] ?? array() );
456 foreach ( $source as $value ) {
457 if ( ! isset( $value['product'] ) || ! is_string( $value['product'] ) || '' === $value['product'] ) {
458 // Skip empty product entries.
459 continue;
460 }
461 $custom_widget_settings[] = array(
462 'customLocationSelector' => $value['customLocationSelector'] ?? $value['selForTarget'] ?? '',
463 'product' => $value['product'],
464 'displayWidget' => $value['displayWidget'] ?? true,
465 'customWidgetStyle' => $value['customWidgetStyle'] ?? $value['widgetStyles'] ?? '',
466 );
467 }
468
469 $cart_widget_product = $data['widgetSettings']['widgetSettingsForCart']['widgetProduct'] ?? '';
470 if ( '' === $cart_widget_product ) {
471 $cart_widget_product = $this->get_first_payment_method_product( WidgetSettingsService::WIDGET_SUPPORTED_CATEGORIES_ON_CART_PAGE );
472 }
473 $listing_widget_product = $data['widgetSettings']['widgetSettingsForListing']['widgetProduct'] ?? '';
474 if ( '' === $listing_widget_product ) {
475 $listing_widget_product = $this->get_first_payment_method_product( WidgetSettingsService::MINI_WIDGET_SUPPORTED_CATEGORIES_ON_PRODUCT_LISTING_PAGE );
476 }
477
478 return array(
479 'id' => (int) $row['id'],
480 'widgetSettings' => array(
481 'displayOnProductPage' => $data['widgetSettings']['displayOnProductPage'] ?? false,
482 'showInstallmentsInProductListing' => $data['widgetSettings']['showInstallmentsInProductListing'] ?? false,
483 'showInstallmentsInCartPage' => $data['widgetSettings']['showInstallmentsInCartPage'] ?? false,
484 'widgetConfiguration' => $data['widgetSettings']['widgetConfiguration'] ?? '',
485 'widgetSettingsForProduct' => array(
486 'priceSelector' => $data['widgetSettings']['widgetSettingsForProduct']['priceSelector'] ?? ( $data['widgetSettings']['widgetLocationConfiguration']['selForPrice'] ?? '' ),
487 'locationSelector' => $data['widgetSettings']['widgetSettingsForProduct']['locationSelector'] ?? ( $data['widgetSettings']['widgetLocationConfiguration']['selForDefaultLocation'] ?? '' ),
488 'altPriceSelector' => $data['widgetSettings']['widgetSettingsForProduct']['altPriceSelector'] ?? ( $data['widgetSettings']['widgetLocationConfiguration']['selForAltPrice'] ?? '' ),
489 'altPriceTriggerSelector' => $data['widgetSettings']['widgetSettingsForProduct']['altPriceTriggerSelector'] ?? ( $data['widgetSettings']['widgetLocationConfiguration']['selForAltPriceTrigger'] ?? '' ),
490 'customWidgetSettings' => $custom_widget_settings,
491 ),
492 'widgetSettingsForCart' => array(
493 'priceSelector' => $data['widgetSettings']['widgetSettingsForCart']['priceSelector'] ?? ( $data['widgetSettings']['cartMiniWidgetConfiguration']['selForPrice'] ?? '' ),
494 'locationSelector' => $data['widgetSettings']['widgetSettingsForCart']['locationSelector'] ?? ( $data['widgetSettings']['cartMiniWidgetConfiguration']['selForDefaultLocation'] ?? '' ),
495 'widgetProduct' => $cart_widget_product,
496 ),
497 'widgetSettingsForListing' => array(
498 'priceSelector' => $data['widgetSettings']['widgetSettingsForListing']['priceSelector'] ?? ( $data['widgetSettings']['listingMiniWidgetConfiguration']['selForPrice'] ?? '' ),
499 'locationSelector' => $data['widgetSettings']['widgetSettingsForListing']['locationSelector'] ?? ( $data['widgetSettings']['listingMiniWidgetConfiguration']['selForDefaultLocation'] ?? '' ),
500 'widgetProduct' => $listing_widget_product,
501 ),
502 ),
503 );
504 }
505
506
507 /**
508 * Process an string array and sanitize each entry
509 *
510 * @param mixed $arr
511 * @return string[]
512 */
513 private function sanitize_array_of_strings( $arr ): array {
514 $result = array();
515 if ( is_array( $arr ) ) {
516 foreach ( $arr as $item ) {
517 if ( is_string( $item ) && '' !== $item ) {
518 $result[] = $item;
519 }
520 }
521 }
522 return $result;
523 }
524
525 /**
526 * Get GeneralSettings from the database.
527 *
528 * @return array{
529 * id: int,
530 * generalSettings: array{
531 * sendOrderReportsPeriodicallyToSeQura: bool,
532 * showSeQuraCheckoutAsHostedPage: bool,
533 * allowedIPAddresses: string[],
534 * excludedProducts: string[],
535 * excludedCategories: string[],
536 * defaultServicesEndDate: string
537 * }
538 * }|null GeneralSettings or null if not found.
539 */
540 private function get_general_settings(): ?array {
541 // @phpstan-ignore-next-line
542 $query = $this->db->prepare( 'SELECT `id`, `data` FROM ' . $this->entity_table . ' WHERE `type` = %s AND `index_1` = %s LIMIT 1', 'GeneralSettings', $this->store_context->getStoreId() );
543 $row = $this->db->get_row( $query, ARRAY_A );
544 $data = isset( $row['data'] ) && is_string( $row['data'] ) ? json_decode( $row['data'], true ) : null;
545
546 if ( ! is_array( $data ) || ! isset( $row['id'] ) ) {
547 return null;
548 }
549
550 if ( GeneralSettings::class === strval( $data['class_name'] ?? '' ) ) {
551 // Data is already in the new format, nothing to migrate.
552 return null;
553 }
554
555 return array(
556 'id' => (int) $row['id'],
557 'generalSettings' => array(
558 'sendOrderReportsPeriodicallyToSeQura' => ! empty( $data['generalSettings']['sendOrderReportsPeriodicallyToSeQura'] ?? false ),
559 'showSeQuraCheckoutAsHostedPage' => ! empty( $data['generalSettings']['showSeQuraCheckoutAsHostedPage'] ?? false ),
560 'allowedIPAddresses' => $this->sanitize_array_of_strings( $data['generalSettings']['allowedIPAddresses'] ?? array() ),
561 'excludedProducts' => $this->sanitize_array_of_strings( $data['generalSettings']['excludedProducts'] ?? array() ),
562 'excludedCategories' => $this->sanitize_array_of_strings( $data['generalSettings']['excludedCategories'] ?? array() ),
563 'defaultServicesEndDate' => strval( $data['generalSettings']['defaultServicesEndDate'] ?? '' ),
564 ),
565 );
566 }
567
568 /**
569 * Get the product name of the first payment method available.
570 *
571 * @param array $categories List of categories to filter payment methods.
572 * @return string Product name or empty string if no payment methods found.
573 */
574 private function get_first_payment_method_product( $categories ): string {
575 if ( null === $this->payment_methods ) {
576 // @phpstan-ignore-next-line
577 $query = $this->db->prepare( 'SELECT `data` FROM ' . $this->entity_table . ' WHERE `type` = %s AND `index_1` = %s', 'PaymentMethod', $this->store_context->getStoreId() );
578 $rows = $this->db->get_results( $query, ARRAY_A );
579 if ( ! is_array( $rows ) ) {
580 return '';
581 }
582 $this->payment_methods = array();
583 foreach ( $rows as $row ) {
584 /**
585 * Data
586 *
587 * @var array{
588 * seQuraPaymentMethod: array{
589 * product: string,
590 * category: string
591 * }
592 * }|null $data
593 */
594 $data = isset( $row['data'] ) && is_string( $row['data'] ) ? json_decode( $row['data'], true ) : null;
595 $data = $data['seQuraPaymentMethod'] ?? null;
596 if ( is_array( $data )
597 && isset( $data['product'], $data['category'] )
598 && is_string( $data['product'] )
599 && is_string( $data['category'] )
600 && '' !== $data['product']
601 && '' !== $data['category']
602 ) {
603 // Check for duplicates before adding.
604 $found = false;
605 foreach ( $this->payment_methods as $pm ) {
606 if ( $pm['product'] === $data['product'] ) {
607 $found = true;
608 break;
609 }
610 }
611 if ( $found ) {
612 continue;
613 }
614
615 $this->payment_methods[] = array(
616 'product' => $data['product'],
617 'category' => $data['category'],
618 );
619 }
620 }
621
622 // Reorder payment methods so product 'pp3' comes first if available.
623 usort(
624 $this->payment_methods,
625 function ( $a, $b ) {
626 if ( 'pp3' === $a['product'] ) {
627 return -1;
628 }
629 if ( 'pp3' === $b['product'] ) {
630 return 1;
631 }
632 return 0;
633 }
634 );
635 }
636
637 foreach ( $this->payment_methods as $payment_method ) {
638 if ( in_array( $payment_method['category'], $categories, true ) ) {
639 return $payment_method['product'];
640 }
641 }
642 return '';
643 }
644 }
645