PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.5.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.5.5
1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 All 49 releases
fluent-cart / app / Helpers / Helper.php

Helper.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.5.5, at app/Helpers/Helper.php

1,950 lines 64.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Helpers;
4
5 use FluentCart\Api\Confirmation;
6 use FluentCart\Api\CurrencySettings;
7 use FluentCart\Api\StoreSettings;
8 use FluentCart\Api\Taxonomy;
9 use FluentCart\App\App;
10 use FluentCart\App\CPT\FluentProducts;
11 use FluentCart\App\Models\Customer;
12 use FluentCart\App\Models\ProductVariation;
13 use FluentCart\App\Models\User;
14 use FluentCart\App\Services\DateTime\DateTime;
15 use FluentCart\App\Services\Localization\LocalizationManager;
16 use FluentCart\App\Services\Translations\TransStrings;
17 use FluentCart\App\Services\URL;
18 use FluentCart\Framework\Http\URL as BaseUrl;
19 use FluentCart\Framework\Support\Arr;
20
21 class Helper
22 {
23 const PRODUCT_TYPE_SIMPLE = 'simple';
24 const PRODUCT_TYPE_SIMPLE_VARIATION = 'simple_variations';
25 const PRODUCT_TYPE_ADVANCE_VARIATION = 'advanced_variations';
26
27 const INSTANT_CHECKOUT_URL_PARAM = 'fct_cart_hash';
28
29 const IN_STOCK = 'in-stock';
30 const OUT_OF_STOCK = 'out-of-stock';
31
32 const ROLE_CAPABILITY_PREFIX = 'fluent_cart/permissions/';
33
34 const USER_ROLE = 'fluent_cart_customer';
35
36 const MIN_INSTALLMENT_TIMES = 2;
37
38 /**
39 * An installment plan must bill at least twice. times = 0 means unlimited
40 * (a plain recurring subscription) and times = 1 collects a single payment,
41 * which is a one-time purchase, not an installment plan.
42 *
43 * @param array $otherInfo A variant's other_info payload
44 * @return string|null Error message, or null when valid
45 */
46 public static function installmentTimesError($otherInfo)
47 {
48 if (Arr::get($otherInfo, 'installment', 'no') !== 'yes') {
49 return null;
50 }
51
52 $times = Arr::get($otherInfo, 'times', 0);
53
54 if (!is_numeric($times) || (int)$times < static::MIN_INSTALLMENT_TIMES) {
55 return __('Installment count must be 2 or more. A single installment is just a one-time payment — set the payment type to one-time instead.', 'fluent-cart');
56 }
57
58 return null;
59 }
60
61 public static function getUidSerial()
62 {
63 static $id = 0;
64
65 $id = $id + 1;
66
67 return $id;
68
69 }
70
71 public static function getRestInfo()
72 {
73 $app = App::getInstance();
74
75 $ns = $app->config->get('app.rest_namespace');
76 $ver = $app->config->get('app.rest_version');
77
78 return [
79 'base_url' => self::getBaseRestUrl(),
80 'url' => self::getFullRestUrl($ns, $ver),
81 'nonce' => wp_create_nonce('wp_rest'),
82 'namespace' => $ns,
83 'version' => $ver,
84 ];
85 }
86
87
88 /**
89 * Get base rest url by examining the permalink.
90 *
91 * @see https://wordpress.stackexchange.com/questions/273144/can-i-use-rest-api-on-plain-permalink-format
92 *
93 * @return string
94 */
95 protected static function getBaseRestUrl()
96 {
97 if (get_option('permalink_structure')) {
98 return esc_url_raw(rest_url());
99 }
100
101 return esc_url_raw(
102 rtrim(get_site_url(), '/') . "/?rest_route=/"
103 );
104 }
105
106 /**
107 * Get the full rest url by examining the permalink
108 * (full means, including the namespace/version).
109 *
110 * @see https://wordpress.stackexchange.com/questions/273144/can-i-use-rest-api-on-plain-permalink-format
111 *
112 * @return string
113 */
114 protected static function getFullRestUrl($ns, $ver)
115 {
116 if (get_option('permalink_structure')) {
117 return esc_url_raw(rest_url($ns . '/' . $ver));
118 }
119
120 return esc_url_raw(
121 rtrim(get_site_url(), '/') . "/?rest_route=/{$ns}/{$ver}"
122 );
123 }
124
125
126 public static function shopConfig($key = false)
127 {
128 /**
129 * todo - need to review for more improvement : AR
130 */
131 $currencySettings = (fluentCart(CurrencySettings::class))->get();
132 $storeSettings = (new StoreSettings())->get([
133 'store_name', 'store_logo', 'weight_unit', 'dimension_unit'
134 ]);
135 $storeSettings['shipping_packages'] = self::getShippingPackages();
136
137 $settings = array_merge($currencySettings, $storeSettings);
138
139
140 if (!$key) {
141 return $settings;
142 }
143
144 if (is_array($key)) {
145 return Arr::only($settings, $key);
146 } else {
147 return Arr::get($settings, $key);
148 }
149
150 }
151
152 /**
153 * Convert weight between units using grams as base.
154 *
155 * @param float $value
156 * @param string $fromUnit
157 * @param string $toUnit
158 * @return float
159 */
160 public static function convertWeight($value, $fromUnit, $toUnit)
161 {
162 if ($fromUnit === $toUnit || !$value) {
163 return (float)$value;
164 }
165
166 $toGrams = [
167 'g' => 1,
168 'kg' => 1000,
169 'lbs' => 453.592,
170 'oz' => 28.3495,
171 ];
172
173 $fromFactor = isset($toGrams[$fromUnit]) ? $toGrams[$fromUnit] : 1;
174 $toFactor = isset($toGrams[$toUnit]) ? $toGrams[$toUnit] : 1;
175
176 $grams = $value * $fromFactor;
177 return round($grams / $toFactor, 6);
178 }
179
180 /**
181 * Get all shipping packages from store settings.
182 *
183 * @return array
184 */
185 public static function getShippingPackages()
186 {
187 static $packages = null;
188 if ($packages === null) {
189 $packages = fluent_cart_get_option('shipping_packages', []);
190 }
191 return $packages ?: [];
192 }
193
194 /**
195 * Get the default shipping package.
196 *
197 * @return array|null
198 */
199 public static function getDefaultPackage()
200 {
201 $packages = self::getShippingPackages();
202 foreach ($packages as $package) {
203 if (!empty($package['is_default'])) {
204 return $package;
205 }
206 }
207 return null;
208 }
209
210 /**
211 * Find a shipping package by slug.
212 *
213 * @param string $slug
214 * @return array|null
215 */
216 public static function getPackageBySlug($slug)
217 {
218 if (!$slug) {
219 return self::getDefaultPackage();
220 }
221 $packages = self::getShippingPackages();
222 foreach ($packages as $package) {
223 if (isset($package['slug']) && $package['slug'] === $slug) {
224 return $package;
225 }
226 }
227 return self::getDefaultPackage();
228 }
229
230 public static function invoiceSettings($key = false)
231 {
232 $settings = [
233 'invoice_prefix' => 'AS-',
234 ];
235 if (!$key) {
236 return $settings;
237 }
238
239 return Arr::get($settings, $key);
240 }
241
242 public static function getOrderStatuses()
243 {
244 $statuses = apply_filters_deprecated('fluent-cart/order_statuses', [
245 [
246 'on-hold' => __('On Hold', 'fluent-cart'),
247 'processing' => __('Processing', 'fluent-cart'),
248 'completed' => __('Completed', 'fluent-cart'),
249 //'archived' => __('Archived', 'fluent-cart'),
250 'cancelled' => __('Cancelled', 'fluent-cart'),
251 ], []
252 ], '1.3.16', 'fluent_cart/order_statuses', 'Use fluent_cart/order_statuses instead of fluent-cart/order_statuses. It will be removed in v1.4.3.');
253
254 return apply_filters('fluent_cart/order_statuses', $statuses, []);
255 }
256
257 public static function getEditableOrderStatuses()
258 {
259 $statuses = apply_filters_deprecated('fluent-cart/editable_order_statuses', [
260 [
261 'on-hold' => __('On Hold', 'fluent-cart'),
262 'processing' => __('Processing', 'fluent-cart'),
263 'completed' => __('Completed', 'fluent-cart'),
264 // 'archived' => __('Archived', 'fluent-cart'),
265 'cancelled' => __('Cancelled', 'fluent-cart')
266 ], []
267 ], '1.3.16', 'fluent_cart/editable_order_statuses', 'Use fluent_cart/editable_order_statuses instead of fluent-cart/editable_order_statuses. It will be removed in v1.4.3.');
268
269 return apply_filters('fluent_cart/editable_order_statuses', $statuses, []);
270 }
271
272 public static function getEditableCustomerStatuses()
273 {
274 $statuses = apply_filters_deprecated('fluent-cart/editable_customer_statuses', [
275 [
276 'active' => __('Active', 'fluent-cart'),
277 'inactive' => __('Inactive', 'fluent-cart'),
278 ], []
279 ], '1.3.16', 'fluent_cart/editable_customer_statuses', 'Use fluent_cart/editable_customer_statuses instead of fluent-cart/editable_customer_statuses. It will be removed in v1.4.3.');
280
281 return apply_filters('fluent_cart/editable_customer_statuses', $statuses, []);
282 }
283
284 public static function getShippingStatuses()
285 {
286 $statuses = apply_filters_deprecated('fluent-cart/shipping_statuses', [
287 [
288 'unshipped' => __('Unshipped', 'fluent-cart'),
289 'shipped' => __('Shipped', 'fluent-cart'),
290 'delivered' => __('Delivered', 'fluent-cart'),
291 'unshippable' => __('Unshippable', 'fluent-cart'),
292 ], []
293 ], '1.3.16', 'fluent_cart/shipping_statuses', 'Use fluent_cart/shipping_statuses instead of fluent-cart/shipping_statuses. It will be removed in v1.4.3.');
294
295 return apply_filters('fluent_cart/shipping_statuses', $statuses, []);
296 }
297
298 public static function getEditableShippingStatuses()
299 {
300 $statuses = apply_filters_deprecated('fluent-cart/editable_order_statuses', [
301 [
302 'unshipped' => __('Unshipped', 'fluent-cart'),
303 'shipped' => __('Shipped', 'fluent-cart'),
304 'delivered' => __('Delivered', 'fluent-cart'),
305 'unshippable' => __('Unshippable', 'fluent-cart'),
306 ], []
307 ], '1.3.16', 'fluent_cart/editable_shipping_statuses', 'Use fluent_cart/editable_shipping_statuses instead of fluent-cart/editable_order_statuses. It will be removed in v1.4.3.');
308
309 return apply_filters('fluent_cart/editable_shipping_statuses', $statuses, []);
310 }
311
312 public static function getOrderSuccessStatuses()
313 {
314 return [
315 'completed',
316 // 'archived',
317 'processing',
318 ];
319 }
320
321 public static function getOrderFailedStatuses()
322 {
323 return [
324 'failed',
325 //'refunded',
326 'cancelled',
327 ];
328 }
329
330 public static function getTransactionSuccessStatuses()
331 {
332 return [
333 'paid',
334 ];
335 }
336
337 public static function getTransactionStatuses($withLabel = true)
338 {
339 $statuses = apply_filters_deprecated('fluent-cart/transaction_statuses', [
340 [
341 'pending' => __('Pending', 'fluent-cart'),
342 'paid' => __('Paid', 'fluent-cart'),
343 'require_capture' => __('Authorized (Require Capture)', 'fluent-cart'),
344 'failed' => __('Failed', 'fluent-cart'),
345 'refunded' => __('Refunded', 'fluent-cart'),
346 'active' => __('Active', 'fluent-cart'),
347 ], []
348 ], '1.3.16', 'fluent_cart/transaction_statuses', 'Use fluent_cart/transaction_statuses instead of fluent-cart/transaction_statuses. It will be removed in v1.4.3.');
349
350 $statuses = apply_filters('fluent_cart/transaction_statuses', $statuses, []);
351
352 if ($withLabel) {
353 return $statuses;
354 }
355
356 return array_keys($statuses);
357 }
358
359 public static function getEditableTransactionStatuses($withLabel = true)
360 {
361 $statuses = apply_filters_deprecated('fluent-cart/editable_transaction_statuses', [
362 [
363 'pending' => __('Pending', 'fluent-cart'),
364 'paid' => __('Paid', 'fluent-cart'),
365 'failed' => __('Failed', 'fluent-cart'),
366 'refunded' => __('Refunded', 'fluent-cart'),
367 ], []
368 ], '1.3.16', 'fluent_cart/editable_transaction_statuses', 'Use fluent_cart/editable_transaction_statuses instead of fluent-cart/editable_transaction_statuses. It will be removed in v1.4.3.');
369
370 $statuses = apply_filters('fluent_cart/editable_transaction_statuses', $statuses, []);
371
372 if ($withLabel) {
373 return $statuses;
374 }
375
376 return array_keys($statuses);
377 }
378
379 public static function loadSpoutLib()
380 {
381 static $loaded;
382
383 if ($loaded) {
384 return $loaded;
385 }
386
387 require_once FLUENTCART_PLUGIN_PATH . 'app/Services/Libs/Spout/Autoloader/autoload.php';
388
389 return true;
390 }
391
392 public static function productStatuses($withLabel = true): array
393 {
394 $statues = [
395 'publish' => __('Publish', 'fluent-cart'),
396 'draft' => __('Draft', 'fluent-cart'),
397 'future' => __('Scheduled', 'fluent-cart'),
398 'private' => __('Private', 'fluent-cart'),
399 'trash' => __('Trashed', 'fluent-cart'),
400 ];
401
402 if ($withLabel) {
403 return $statues;
404 }
405
406 return array_keys($statues);
407 }
408
409 public static function productAdminAllStatuses()
410 {
411 $statuses = self::productStatuses();
412 unset($statuses['trash']);
413 return array_keys($statuses);
414 }
415
416 public static function getCartDriver()
417 {
418 return 'db';
419 }
420
421 /**
422 * Convert an amount to a formatted decimal string.
423 *
424 * @param float $amount The amount to convert. (required)
425 * @param bool $withCurrency Whether to include the currency symbol. (optional, default: true)
426 * @param string|null $currencyCode The currency code to use. (optional, default: null)
427 * @param bool $formatted Whether to format the amount. (optional, default: true)
428 * @param bool $showDecimals Whether to show decimal places. (optional, default: true)
429 * @param bool $thousand_separator Whether to include thousand separators. (optional, default: true)
430 *
431 * @return string The formatted amount.
432 *
433 * @dev Note: If you don't want the thousand separator to be applied,
434 * you need to set $formatted to true and $thousand_separator to false.
435 *
436 * @hook fluent_cart/hide_unnecessary_decimals - Filter to control whether unnecessary decimals (like .00) should be hidden.
437 * Usage: add_filter('fluent_cart/hide_unnecessary_decimals', '__return_true'); // This will show 10 instead of 10.00
438 */
439 public static function toDecimal($amount, $withCurrency = true, $currencyCode = null, $formatted = true, $showDecimals = true, $thousand_separator = true, $withTranslatedDigit = true)
440 {
441 if (!is_numeric($amount)) {
442 return $amount;
443 }
444
445 // Set default decimal places to 2
446 $decimal = 2;
447
448 // Check if the shop is using a zero-decimal currency
449 if (self::shopConfig('is_zero_decimal')) {
450 $decimal = 0;
451 }
452
453 // Use provided or default currency code
454 if (!$currencyCode) {
455 $currencyCode = self::shopConfig('currency');
456 }
457
458 // Get currency sign
459 $sign = CurrenciesHelper::getCurrencySign($currencyCode);
460
461 $amount = floatVal($amount / 100);
462
463 // If $showDecimals is false, we override decimal places to 0
464 if (!$showDecimals) {
465 $decimal = 0;
466 }
467
468 // Format the amount based on the decimal configuration
469 if ($formatted) {
470 $decimal_separator = self::shopConfig('decimal_separator') === 'comma' ? ',' : '.';
471
472 $thousand_separator = $decimal_separator === ',' ? '.' : ',';
473
474 // Check if we should hide unnecessary decimal places (e.g., 10.00 -> 10)
475 $hideUnnecessaryDecimals = apply_filters('fluent_cart/hide_unnecessary_decimals', false, [
476 'amount' => $amount,
477 'decimal' => $decimal
478 ]);
479
480 $amount = number_format(
481 $amount,
482 $decimal,
483 $decimal_separator,
484 $thousand_separator
485 );
486
487 if ($hideUnnecessaryDecimals && $decimal > 0) {
488 // Remove trailing zeros only from the decimal portion
489 $parts = explode($decimal_separator, $amount);
490 if (count($parts) === 2) {
491 $parts[1] = rtrim($parts[1], '0');
492 if ($parts[1] === '') {
493 $amount = $parts[0];
494 } else {
495 $amount = $parts[0] . $decimal_separator . $parts[1];
496 }
497 }
498 }
499
500 if ($withTranslatedDigit) {
501 $amount = self::translateNumber($amount);
502 }
503 }
504
505 // If $withCurrency is false, just return the formatted amount
506 if (!$withCurrency) {
507 return $amount;
508 }
509
510 // Get currency position and return formatted amount with currency
511 $position = self::shopConfig('currency_position');
512
513 switch ($position) {
514 case 'before':
515 return $sign . $amount;
516 case 'after':
517 return $amount . $sign;
518 case 'iso_before':
519 return $currencyCode . ' ' . $amount;
520 case 'iso_after':
521 return $amount . ' ' . $currencyCode;
522 case 'symbool_before_iso':
523 return $sign . $amount . ' ' . $currencyCode;
524 case 'symbool_after_iso':
525 return $currencyCode . ' ' . $amount . $sign;
526 case 'symbool_and_iso':
527 return $currencyCode . ' ' . $sign . $amount;
528 default:
529 return $sign . $amount;
530 }
531 }
532
533 public static function toCent($amount): int
534 {
535 if (!is_numeric($amount)) {
536 return 0;
537 }
538
539 $amount = floatval($amount) * 100; // Convert to float and multiply
540 $amount = (int)round($amount); // Round to nearest integer, then cast
541 return $amount;
542 }
543
544 public static function toDecimalWithoutComma($amount)
545 {
546
547 if (!is_numeric($amount)) {
548 return 0;
549 }
550
551 // Convert to float and divide by 100
552 $result = floatval($amount) / 100;
553
554 // Ensure exactly two decimal places
555 return round($result, 2);
556 }
557
558 public static function getCustomerByUser($user)
559 {
560 if (is_numeric($user)) {
561 $user = get_user_by('ID', $user);
562 }
563
564 if (!$user) {
565 return null;
566 }
567
568 return Customer::query()->where('user_id', $user->ID)
569 ->orWhere('email', $user->user_email)
570 ->first();
571 }
572
573 /**
574 * @param array $order_data
575 *
576 * @return array return ['billing_address','shipping_address','others'];
577 */
578
579 /**
580 *
581 * @return string
582 */
583 public static function getProductImageBaseUri(): string
584 {
585 $uploads = wp_upload_dir();
586
587 return $uploads['baseurl'] . '/' . FLUENTCART_UPLOAD_DIR . '/product_image/';
588 }
589
590 public static function getProductImageBaseDir()
591 {
592 $uploads = wp_upload_dir();
593
594 return $uploads['basedir'] . '/' . FLUENTCART_UPLOAD_DIR . '/product_image/';
595 }
596
597 public static function getAvailableCurrencyList()
598 {
599 $currencies = apply_filters_deprecated('fluent-cart/available_currencies', [
600 [
601 'BDT' => [
602 "label" => __('Bangladeshi Taka', 'fluent-cart'),
603 "value" => 'BDT',
604 "symbol" => '৳',
605 ],
606 'USD' => [
607 "label" => __('United State Dollar', 'fluent-cart'),
608 "value" => 'USD',
609 "symbol" => '$',
610 ],
611 'GBP' => [
612 "label" => __('United Kingdom', 'fluent-cart'),
613 "value" => 'GBP',
614 "symbol" => '£',
615 ],
616 ], []
617 ], '1.3.16', 'fluent_cart/available_currencies', 'Use fluent_cart/available_currencies instead of fluent-cart/available_currencies. It will be removed in v1.4.3.');
618
619 return apply_filters('fluent_cart/available_currencies', $currencies, []);
620 }
621
622 public static function getSymbolForCurrency($currency = 'BDT')
623 {
624
625 $symbol = '৳';
626 $list = self::getAvailableCurrencyList();
627
628 return $list[$currency]['symbol'] ?? $symbol;
629 }
630
631 public function getConfirmationSettings()
632 {
633 return (new Confirmation())->get();
634 }
635
636
637 /**
638 *
639 * @param $remove
640 * @return string
641 */
642 public static function getCheckoutPageLinkAfterRemovingGetParams($remove = [])
643 {
644
645 global $fct_store;
646
647 $link = Arr::get($fct_store, 'checkout_link');
648
649 $link .= '?';
650
651 if (!empty($remove)) {
652 $params = App::request()->all();
653
654 foreach ($params as $key => $val) {
655
656 if (!in_array($key, $remove)) {
657
658 $link .= $key . '=' . $val . '&';
659 }
660 }
661 }
662
663 return rtrim($link, '&');
664 }
665
666
667 /**
668 *
669 * @return bool
670 */
671 public static function isSingleProductPage(): bool
672 {
673 return is_singular([FluentProducts::CPT_NAME]);
674 }
675
676
677 public static function isTrue($array, $key)
678 {
679 $value = $array[$key] ?? false;
680 if (is_bool($value)) {
681 return $value;
682 }
683 if ($value === 'false' || !$value) {
684 return false;
685 }
686 return true;
687 }
688
689 public static function is_valid_json($string): bool
690 {
691 if (!is_string($string)) {
692 return false;
693 }
694
695 $trimmed = trim($string);
696
697 // Basic check: must start with { or [ and end with } or ]
698 if (!preg_match('/^(\{.*\}|\[.*\])$/s', $trimmed)) {
699 return false;
700 }
701
702 json_decode($trimmed);
703 return json_last_error() === JSON_ERROR_NONE;
704 }
705
706 public static function getStockStatuses($withLabel = true)
707 {
708 $statues = [
709 'in-stock' => __('In Stock', 'fluent-cart'),
710 'out-of-stock' => __('Out Of Stock', 'fluent-cart'),
711 ];
712
713 if ($withLabel) {
714 return $statues;
715 }
716
717 return array_keys($statues);
718 }
719
720 public static function getFulfilmentTypes($withLabel = true)
721 {
722 $statues = [
723 'physical' => __('Physical', 'fluent-cart'),
724 'digital' => __('Digital', 'fluent-cart'),
725 ];
726
727 if ($withLabel) {
728 return $statues;
729 }
730
731 return array_keys($statues);
732 }
733
734 public static function getVariationTypes($withLabel = true)
735 {
736 // advanced_variations is advertised in free too so the variation-type
737 // dropdown can offer it (shown Pro-locked with a crown and an upgrade
738 // modal while Pro is inactive). Pro re-registers the same key via the
739 // filter below when active.
740 $types = [
741 'simple' => __('Simple', 'fluent-cart'),
742 'simple_variations' => __('Simple Variations', 'fluent-cart'),
743 'advanced_variations' => __('Advanced Variations', 'fluent-cart'),
744 ];
745
746 $types = apply_filters('fluent_cart/variation_types', $types);
747
748 if ($withLabel) {
749 return $types;
750 }
751
752 return array_keys($types);
753 }
754
755 public static function isValueEncrypted($raw_value)
756 {
757 if (!$raw_value || !is_string($raw_value) || !extension_loaded('openssl')) {
758 return false;
759 }
760
761 // Check if input is valid base64
762 if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $raw_value)) {
763 return false;
764 }
765
766 $decoded = base64_decode($raw_value, true);
767 if ($decoded === false) {
768 return false;
769 }
770
771 // Check if decoded string is long enough for IV
772 $method = 'aes-256-ctr';
773 $ivlen = openssl_cipher_iv_length($method);
774 if (strlen($decoded) < $ivlen) {
775 return false;
776 }
777
778 $iv = substr($decoded, 0, $ivlen);
779 $ciphertext = substr($decoded, $ivlen);
780
781 $key = (defined('FLUENT_CART_ENCRYPTION_KEY'))
782 ? FLUENT_CART_ENCRYPTION_KEY
783 : ((defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY)
784 ? LOGGED_IN_KEY
785 : 'this-is-a-fallback-key-but-not-secure');
786 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT)
787 ? LOGGED_IN_SALT
788 : 'this-is-a-fallback-salt-but-not-secure';
789
790 $value = openssl_decrypt($ciphertext, $method, $key, 0, $iv);
791 if ($value === false) {
792 return false;
793 }
794
795
796 return substr($value, -strlen($salt)) === $salt;
797 }
798
799 public static function encryptKey($value)
800 {
801 if (!$value) {
802 return $value;
803 }
804
805 if (!extension_loaded('openssl')) {
806 return $value;
807 }
808
809 if (self::isValueEncrypted($value)) {
810 return $value;
811 }
812
813 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
814
815 if (defined('FLUENT_CART_ENCRYPTION_KEY')) {
816 $key = FLUENT_CART_ENCRYPTION_KEY;
817 } else {
818 $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
819 }
820
821 $method = 'aes-256-ctr';
822 $ivlen = openssl_cipher_iv_length($method);
823 $iv = openssl_random_pseudo_bytes($ivlen);
824
825 $raw_value = openssl_encrypt($value . $salt, $method, $key, 0, $iv);
826 if (!$raw_value) {
827 return false;
828 }
829
830 return base64_encode($iv . $raw_value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
831 }
832
833 public static function decryptKey($raw_value)
834 {
835
836 if (!$raw_value) {
837 return $raw_value;
838 }
839
840 if (!extension_loaded('openssl')) {
841 return $raw_value;
842 }
843
844 $decoded = base64_decode($raw_value, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
845
846 if ($decoded === false) {
847 return $raw_value;
848 }
849
850 $method = 'aes-256-ctr';
851 $ivlen = openssl_cipher_iv_length($method);
852
853 if (strlen($decoded) <= $ivlen) {
854 return $raw_value;
855 }
856
857 $iv = substr($decoded, 0, $ivlen);
858
859 $ciphertext = substr($decoded, $ivlen);
860
861 if (defined('FLUENT_CART_ENCRYPTION_KEY')) {
862 $key = FLUENT_CART_ENCRYPTION_KEY;
863 } else {
864 $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
865 }
866
867 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
868
869 $decrypted = openssl_decrypt($ciphertext, $method, $key, 0, $iv);
870 if (!$decrypted || substr($decrypted, -strlen($salt)) !== $salt) {
871 return false;
872 }
873
874 return substr($decrypted, 0, -strlen($salt));
875 }
876
877 /**
878 * @return array
879 * For kses_post to allow some html tags
880 */
881 public static function allowedHTMLForCheckout(): array
882 {
883
884 $allowedTags = [
885 'svg' => [
886 'class' => true,
887 'aria-hidden' => true,
888 'aria-labelledby' => true,
889 'role' => true,
890 'xmlns' => true,
891 'width' => true,
892 'height' => true,
893 'viewbox' => true,
894 'fill' => true
895 ],
896 'g' => [
897 'fill' => true
898 ],
899 'title' => ['title' => true],
900 'path' => [
901 'd' => true,
902 'fill' => true,
903 'stroke' => true,
904 'fill-rule' => true,
905 'clip-rule' => true,
906 ],
907 ];
908
909 foreach (['input', 'label', 'div', 'span', 'p', 'select', 'option', 'textarea', 'button', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as $tag) {
910 $allowedTags[$tag] = [
911 'selected',
912 'type' => [],
913 "selected='selected'" => [],
914 'name' => [],
915 'value' => [],
916 'autocomplete' => [],
917 'placeholder' => [],
918 'data-required' => [],
919 'data-type' => [],
920 'id' => [],
921 'class' => [],
922 'required' => [],
923 'disabled' => [],
924 'for' => [],
925 'style' => [],
926 'checked' => [],
927 'maxlength' => [],
928 'data-id' => [],
929 'data-country' => [],
930 'data-fluent-cart-checkout-page-form-address-modal-open-button' => [],
931 'data-fluent-cart-checkout-page-form-address-modal-wrapper' => [],
932 'data-fluent-cart-checkout-page-form-input-wrapper' => [],
933 'data-fluent_cart_checkout_error' => [],
934 'data-fluent-cart-checkout-page-form-address-modal-body' => [],
935 'data-fluent-cart-checkout-page-form-address-modal-address-selector-button' => [],
936 'data-fluent-cart-checkout-page-form-address-input' => [],
937 'data-fluent-cart-checkout-page-form-address-select-wrapper' => [],
938 'data-fluent-cart-checkout-page-form-address-modal-close-button' => [],
939 'data-fluent-cart-checkout-page-form-address-modal-address-selector-button-wrapper' => [],
940 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-button' => [],
941 'data-fluent-cart-checkout-page-form-address-modal-apply-button' => [],
942 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-form-wrapper' => [],
943 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-submit-button' => [],
944 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-cancel-button' => [],
945 'data-fluent-cart-address-type' => [],
946 'data-fluent-cart-checkout-page-form-address-info-wrapper' => [],
947 'data-fluent-cart-checkout-page-form-error' => [],
948 'data-fluent-cart-checkout-page-form-section' => [],
949 'data-fluent-cart-checkout-page-discount-container' => [],
950 'data-fluent-cart-checkout-page-final-amount-container' => [],
951 'data-fluent-cart-checkout-page-new-total-amount' => [],
952 'data-fluent-cart-checkout-page-final-amount' => [],
953 'data-fluent-cart-checkout-page-coupon-validate' => [],
954 'data-fluent-cart-checkout-coupon-items-toggle' => [],
955 'data-fluent-cart-checkout-coupon-items-wrapper' => []
956
957
958 ];
959 }
960
961 return $allowedTags;
962 }
963
964 public static function getCouponStatuses()
965 {
966 $statuses = apply_filters_deprecated('fluent-cart/coupon_statuses', [
967 [
968 'active' => __('Active', 'fluent-cart'),
969 'expired' => __('Expired', 'fluent-cart'),
970 'disabled' => __('Disabled', 'fluent-cart'),
971 ], []
972 ], '1.3.16', 'fluent_cart/coupon_statuses', 'Use fluent_cart/coupon_statuses instead of fluent-cart/coupon_statuses. It will be removed in v1.4.3.');
973
974 return apply_filters('fluent_cart/coupon_statuses', $statuses, []);
975 }
976
977 public static function getCouponSuccessStatuses()
978 {
979 return [
980 'active'
981 ];
982 }
983
984
985 /**
986 * Compact subscription terms as text
987 *
988 * Expected $data keys:
989 * - trial_days (int)
990 * - interval ('daily'|'weekly'|'monthly'|'yearly')
991 * - interval_count (int, default 1)
992 * - times (int; 0 = open-ended, >0 = finite number of payments/cycles)
993 * - price (string; formatted with currency)
994 * - signup_fee (string; 0 if none)
995 * - compare_price (string; 0 if none)
996 *
997 * Output examples:
998 * - "30 days free then $100.00 per year"
999 * - "$100 per year + $10 one-time signup fee"
1000 * - "$100/month for 4 months"
1001 * - "30 days free then $100/month for 4 months"
1002 * - "$99 per month"
1003 */
1004 public static function getSubscriptionTermText(array $data, $asHtml = false): string
1005 {
1006 // Fixed interval options
1007 $intervalOptions = static::getAvailableSubscriptionIntervalMaps();
1008
1009 // Normalize / defaults
1010 $trialDays = $data['trial_days'] ?? 0;
1011 $interval = (string)($data['interval'] ? $data['interval'] : 'monthly');
1012
1013 $unit = '';
1014 if (isset($intervalOptions[$interval])) {
1015 $unit = $intervalOptions[$interval];
1016 } else if ($interval) {
1017 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
1018 foreach ($intervalOptions as $option) {
1019 if ($option['value'] === $interval) {
1020 $unit = strtolower($option['label']);
1021 break;
1022 }
1023 }
1024
1025 if (!$unit) {
1026 $unit = strtolower(str_replace(['_', '-'], ' ', $interval));
1027 }
1028 }
1029
1030 if (!$unit) {
1031 $unit = 'year';
1032 }
1033
1034 $count = max(1, (int)($data['interval_count'] ?? 1));
1035 $times = (int)($data['times'] ?? 0);
1036 $price = (string)($data['price'] ?? '');
1037 $signupFee = $data['signup_fee'] ?? '';
1038 $compare = $data['compare_price'] ?? '';
1039
1040 $signupFeeLabel = $data['signup_fee_label'] ?? __('signup fee', 'fluent-cart');
1041
1042 // helpers
1043 $pluralUnit = static function (string $unit, int $n): string {
1044 switch ($unit) {
1045 case 'day':
1046 return _n('day', 'days', $n, 'fluent-cart');
1047 case 'week':
1048 return _n('week', 'weeks', $n, 'fluent-cart');
1049 case 'quarter':
1050 return _n('quarter', 'quarters', $n, 'fluent-cart');
1051 case 'half_year':
1052 return _n('six month', 'six months', $n, 'fluent-cart');
1053 case 'year':
1054 return _n('year', 'years', $n, 'fluent-cart');
1055 case 'month':
1056 return _n('month', 'months', $n, 'fluent-cart');
1057 default:
1058 // For custom intervals, add 's' for plural
1059 return ($n > 1) ? $unit . 's' : $unit;
1060 }
1061 };
1062
1063 // Build the “per …” phrase (e.g., "per month", "per 3 months")
1064 $perPhrase = ($count === 1)
1065 ? sprintf(
1066 /* translators: %s is the singular unit name (e.g., day, month, year) */
1067 __('per %s', 'fluent-cart'),
1068 $pluralUnit($unit, 1)
1069 )
1070 : sprintf(
1071 /* translators: %1$d is the count number, %2$s is the plural unit name (e.g., days, months, years) */
1072 __('per %1$d %2$s', 'fluent-cart'),
1073 $count,
1074 $pluralUnit($unit, $count)
1075 );
1076
1077 // Main price phrase:
1078 // - If monthly installments (times > 0 and unit == month and count == 1): "$X/month for N months"
1079 // - Else if times > 0: "$X per {count unit(s)} for N {cycle(s)}"
1080 // - Else: "$X per {count unit(s)}"
1081 if ($times > 0) {
1082 if ($unit === 'month' && $count === 1) {
1083 $installmentTail = sprintf(
1084 /* translators: %s: pluralized 'month(s)' */
1085 __('for %1$d %2$s', 'fluent-cart'),
1086 $times,
1087 $pluralUnit('month', $times)
1088 );
1089 $main = sprintf(
1090 /* translators: Compact monthly installment, e.g. "$100/month for 4 months" */
1091 __('%1$s/%2$s %3$s', 'fluent-cart'),
1092 $price,
1093 __('month', 'fluent-cart'),
1094 $installmentTail
1095 );
1096 } else {
1097 // Generic installment wording: "for N cycle(s)"
1098 $cyclesTail = sprintf(
1099 /* translators: %1$d is the number of cycles, %2$s is "cycle" or "cycles" */
1100 __('for %1$d %2$s', 'fluent-cart'),
1101 $times,
1102 _n('cycle', 'cycles', $times, 'fluent-cart')
1103 );
1104 $main = sprintf(
1105 /* translators: e.g. "$100 per 3 months for 4 cycles" */
1106 __('%1$s %2$s %3$s', 'fluent-cart'),
1107 $price,
1108 $perPhrase,
1109 $cyclesTail
1110 );
1111 }
1112 } else {
1113 // Open-ended subscription
1114 $main = sprintf(
1115 /* translators: e.g. "$99 per month" */
1116 __('%1$s %2$s', 'fluent-cart'),
1117 $price,
1118 $perPhrase
1119 );
1120 }
1121
1122 // Prefix trial if present: "N days free then …"
1123 if ($trialDays > 0) {
1124 $trialFrag = sprintf(
1125 /* translators: "30 days free then" */
1126 __('%1$d %2$s free then', 'fluent-cart'),
1127 $trialDays,
1128 $pluralUnit('day', $trialDays)
1129 );
1130 $main = $trialFrag . ' ' . $main;
1131 }
1132
1133 // Append signup fee if any: " + $10 one-time signup fee"
1134 if ($signupFee) {
1135 $main .= ' ' . sprintf(
1136 /* translators: e.g. "+ $10 one-time signup fee" */
1137 __('+ %1$s one-time %2$s', 'fluent-cart'),
1138 $signupFee,
1139 $signupFeeLabel
1140 );
1141 }
1142
1143 // No trailing period (matches your examples)
1144 return $main;
1145 }
1146
1147 public static function getTranslatedIntervalUnit(string $unit): string
1148 {
1149 switch ($unit) {
1150 case 'day':
1151 return __('day', 'fluent-cart');
1152 case 'week':
1153 return __('week', 'fluent-cart');
1154 case 'month':
1155 return __('month', 'fluent-cart');
1156 case 'quarter':
1157 return __('quarter', 'fluent-cart');
1158 case 'half_year':
1159 return __('six month', 'fluent-cart');
1160 case 'year':
1161 return __('year', 'fluent-cart');
1162 default:
1163 return $unit;
1164 }
1165 }
1166
1167 public static function generateSubscriptionInfo($otherInfo, $itemPrice, $currencyCode = null): ?string
1168 {
1169 // Convert to array only if it's an object
1170 if (is_object($otherInfo)) {
1171 $otherInfo = json_decode(json_encode($otherInfo), true);
1172 }
1173
1174 $price = self::toDecimal($itemPrice, true, $currencyCode);
1175 $recurringDiscountAmount = Arr::get($otherInfo, 'recurring_discounts.amount', 0);
1176
1177 if ($recurringDiscountAmount) {
1178 $newRecurringAmount = $itemPrice - $recurringDiscountAmount;
1179 $price = "<del>" . $price . "</del> " . self::toDecimal($newRecurringAmount, true, $currencyCode);
1180 }
1181
1182 $repeatInterval = Arr::get($otherInfo, 'repeat_interval', '');
1183 $occurrence = (int)Arr::get($otherInfo, 'times', 0);
1184
1185 $intervalOptions = static::getAvailableSubscriptionIntervalMaps();
1186
1187 $intervalUnit = '';
1188 if (isset($intervalOptions[$repeatInterval])) {
1189 $intervalUnit = $intervalOptions[$repeatInterval];
1190 } else if ($repeatInterval) {
1191 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
1192 foreach ($intervalOptions as $option) {
1193 if ($option['value'] === $repeatInterval) {
1194 $intervalUnit = strtolower($option['label']);
1195 break;
1196 }
1197 }
1198
1199 if (!$intervalUnit) {
1200 $intervalUnit = ucwords(str_replace(['_', '-'], ' ', $repeatInterval));
1201 }
1202 }
1203
1204 $intervalLabel = Helper::getTranslatedIntervalUnit($intervalUnit);
1205
1206 $interval = $intervalUnit
1207 ? sprintf(
1208 /* translators: %s is the interval (e.g., day, week, month, quarter, half_year, year) */
1209 __('per %s', 'fluent-cart'),
1210 $intervalLabel
1211 )
1212 : '';
1213
1214 $time = $intervalUnit
1215 ? $intervalLabel
1216 : '';
1217
1218 $paymentInfo = sprintf(
1219 /* translators: %1$s is the price, %2$s is the interval, %3$s is "until cancel" text, %4$s is the occurrence count, %5$s is the time period */
1220 __('%1$s %2$s, for %3$s %4$s', 'fluent-cart'),
1221 $price,
1222 $interval,
1223 $occurrence,
1224 $time
1225 );
1226
1227 if (empty($occurrence)) {
1228 $paymentInfo = sprintf(
1229 /* translators: %1$s is the price, %2$s is the interval, %3$s is "until cancel" text */
1230 __('%1$s %2$s %3$s', 'fluent-cart'),
1231 $price,
1232 $interval,
1233 __('until cancel', 'fluent-cart')
1234 );
1235 }
1236
1237 return !empty($otherInfo) ? $paymentInfo : null;
1238 }
1239
1240 public static function generateSetupFeeInfo($otherInfo, $asArray = false)
1241 {
1242 // Convert to array if it's an object
1243 if (is_object($otherInfo)) {
1244 $otherInfo = json_decode(json_encode($otherInfo), true);
1245 }
1246
1247 $signupFeeName = Arr::get($otherInfo, 'signup_fee_name', __('Setup Fee', 'fluent-cart'));
1248 $fee = Arr::get($otherInfo, 'signup_fee', 0);
1249 $manageSetupFee = Arr::get($otherInfo, 'manage_setup_fee', 'no');
1250
1251 if ($manageSetupFee !== 'yes' || !$fee) {
1252 return '';
1253 }
1254
1255
1256 if ($originalSetupFee = Arr::get($otherInfo, 'original_signup_fee', 0)) {
1257 if ($fee != $originalSetupFee) {
1258 $title = __('Adjusted setup fee', 'fluent-cart');
1259 $formattedAmount = CurrencySettings::getPriceHtml($fee, null, true, true);
1260
1261 if ($asArray) {
1262 return [
1263 'signup_fee_name' => $title,
1264 'signup_fee' => $fee,
1265 'signup_fee_formatted' => $formattedAmount,
1266 ];
1267 }
1268 return $title . $formattedAmount;
1269 }
1270 }
1271
1272 $formattedAmount = CurrencySettings::getPriceHtml($fee, null, true, true);
1273 if ($asArray) {
1274 return [
1275 'signup_fee_name' => $signupFeeName,
1276 'signup_fee' => $fee,
1277 'signup_fee_formatted' => $formattedAmount,
1278 ];
1279 }
1280
1281
1282 return $signupFeeName . ' ' . $formattedAmount;
1283 }
1284
1285 public static function generateTrialInfo($otherInfo)
1286 {
1287 $trialInfo = '';
1288
1289 $trialDays = Arr::get($otherInfo, 'trial_days', 0);
1290
1291 if ($trialDays && Arr::get($otherInfo, 'is_trial_days_simulated', 'no') !== 'yes') {
1292 $trialInfo = sprintf(
1293 /* translators: %d is the number of trial days */
1294 __('Free Trial: %d days', 'fluent-cart'),
1295 $trialDays
1296 );
1297 }
1298
1299 return apply_filters('fluent_cart/trial_info', $trialInfo, $otherInfo);
1300 }
1301
1302
1303 public static function getCountryList(): array
1304 {
1305 $options = App::getInstance('localization')->countriesOptions();
1306 $options = apply_filters_deprecated('fluent-cart/util/countries', [$options, []], '1.3.16', 'fluent_cart/util/countries', 'Use fluent_cart/util/countries instead of fluent-cart/util/countries. It will be removed in v1.4.3.');
1307
1308 return apply_filters('fluent_cart/util/countries', $options, []);
1309 }
1310
1311 public static function getCountyIsoLists(): array
1312 {
1313 return App::getInstance('localization')->getCountyIsoLists();
1314 }
1315
1316 public static function getCountryCode($country_name)
1317 {
1318 $countries = self::getCountryList();
1319 foreach ($countries as $country) {
1320 if ($country['name'] === $country_name) {
1321 return $country['value'];
1322 }
1323 }
1324 return '';
1325 }
1326
1327 /**
1328 * Get the country's name with country code,
1329 *
1330 * @param $code
1331 * @return string
1332 */
1333 public static function getCountryName($code): string
1334 {
1335 if (!$code || !is_string($code)) {
1336 return '';
1337 }
1338
1339 $countries = self::getCountyIsoLists();
1340
1341 return $countries[$code] ?? $code;
1342 }
1343
1344 public static function languageCodes(): array
1345 {
1346 return $langCodes = [
1347 'AF' => 'fa-AF',
1348 'AL' => 'sq-AL',
1349 'DZ' => 'ar-DZ',
1350 'AS' => 'sm-AS',
1351 'AD' => 'ca-AD',
1352 'AO' => 'pt-AO',
1353 'AR' => 'es-AR',
1354 'AM' => 'hy-AM',
1355 'AU' => 'en-AU',
1356 'AT' => 'de-AT',
1357 'AZ' => 'az-AZ',
1358 'BH' => 'ar-BH',
1359 'BD' => 'bn-BD',
1360 'BY' => 'be-BY',
1361 'BE' => 'nl-BE',
1362 'BZ' => 'en-BZ',
1363 'BJ' => 'fr-BJ',
1364 'BT' => 'dz-BT',
1365 'BO' => 'es-BO',
1366 'BA' => 'bs-BA',
1367 'BW' => 'en-BW',
1368 'BR' => 'pt-BR',
1369 'BN' => 'ms-BN',
1370 'BG' => 'bg-BG',
1371 'BF' => 'fr-BF',
1372 'BI' => 'fr-BI',
1373 'KH' => 'km-KH',
1374 'CM' => 'en-CM',
1375 'CA' => 'en-CA',
1376 'CV' => 'pt-CV',
1377 'CF' => 'fr-CF',
1378 'TD' => 'fr-TD',
1379 'CL' => 'es-CL',
1380 'CN' => 'zh-CN',
1381 'CO' => 'es-CO',
1382 'KM' => 'ar-KM',
1383 'CD' => 'fr-CD',
1384 'CG' => 'fr-CG',
1385 'CR' => 'es-CR',
1386 'CI' => 'fr-CI',
1387 'HR' => 'hr-HR',
1388 'CU' => 'es-CU',
1389 'CY' => 'el-CY',
1390 'CZ' => 'cs-CZ',
1391 'DK' => 'da-DK',
1392 'DJ' => 'fr-DJ',
1393 'DM' => 'en-DM',
1394 'DO' => 'es-DO',
1395 'EC' => 'es-EC',
1396 'EG' => 'ar-EG',
1397 'SV' => 'es-SV',
1398 'GQ' => 'es-GQ',
1399 'ER' => 'ti-ER',
1400 'EE' => 'et-EE',
1401 'ET' => 'am-ET',
1402 'FJ' => 'en-FJ',
1403 'FI' => 'fi-FI',
1404 'FR' => 'fr-FR',
1405 'GA' => 'fr-GA',
1406 'GM' => 'en-GM',
1407 'GE' => 'ka-GE',
1408 'DE' => 'de-DE',
1409 'GH' => 'en-GH',
1410 'GR' => 'el-GR',
1411 'GD' => 'en-GD',
1412 'GT' => 'es-GT',
1413 'GN' => 'fr-GN',
1414 'GW' => 'pt-GW',
1415 'GY' => 'en-GY',
1416 'HT' => 'fr-HT',
1417 'HN' => 'es-HN',
1418 'HU' => 'hu-HU',
1419 'IS' => 'is-IS',
1420 'IN' => 'hi-IN',
1421 'ID' => 'id-ID',
1422 'IR' => 'fa-IR',
1423 'IQ' => 'ar-IQ',
1424 'IE' => 'en-IE',
1425 'IL' => 'he-IL',
1426 'IT' => 'it-IT',
1427 'JM' => 'en-JM',
1428 'JP' => 'ja-JP',
1429 'JO' => 'ar-JO',
1430 'KZ' => 'kk-KZ',
1431 'KE' => 'sw-KE',
1432 'KI' => 'en-KI',
1433 'KR' => 'ko-KR',
1434 'KW' => 'ar-KW',
1435 'KG' => 'ky-KG',
1436 'LA' => 'lo-LA',
1437 'LV' => 'lv-LV',
1438 'LB' => 'ar-LB',
1439 'LS' => 'en-LS',
1440 'LR' => 'en-LR',
1441 'LY' => 'ar-LY',
1442 'LI' => 'de-LI',
1443 'LT' => 'lt-LT',
1444 'LU' => 'lb-LU',
1445 'MG' => 'mg-MG',
1446 'MW' => 'en-MW',
1447 'MY' => 'ms-MY',
1448 'MV' => 'dv-MV',
1449 'ML' => 'fr-ML',
1450 'MT' => 'mt-MT',
1451 'MH' => 'mh-MH',
1452 'MR' => 'ar-MR',
1453 'MU' => 'mfe-MU',
1454 'MX' => 'es-MX',
1455 'FM' => 'en-FM',
1456 'MD' => 'ro-MD',
1457 'MC' => 'fr-MC',
1458 'MN' => 'mn-MN',
1459 'ME' => 'sr-ME',
1460 'MA' => 'ar-MA',
1461 'MZ' => 'pt-MZ',
1462 'NA' => 'en-NA',
1463 'NR' => 'en-NR',
1464 'NP' => 'ne-NP',
1465 'NL' => 'nl-NL',
1466 'NZ' => 'en-NZ',
1467 'NI' => 'es-NI',
1468 'NG' => 'en-NG',
1469 'NO' => 'no-NO',
1470 'OM' => 'ar-OM',
1471 'PK' => 'ur-PK',
1472 'PW' => 'en-PW',
1473 'PA' => 'es-PA',
1474 'PG' => 'en-PG',
1475 'PY' => 'es-PY',
1476 'PE' => 'es-PE',
1477 'PH' => 'en-PH',
1478 'PL' => 'pl-PL',
1479 'PT' => 'pt-PT',
1480 'QA' => 'ar-QA',
1481 'RO' => 'ro-RO',
1482 'RU' => 'ru-RU',
1483 'RW' => 'rw-RW',
1484 'WS' => 'sm-WS',
1485 'SM' => 'it-SM',
1486 'SA' => 'ar-SA',
1487 'SN' => 'fr-SN',
1488 'RS' => 'sr-RS',
1489 'SC' => 'fr-SC',
1490 'SL' => 'en-SL',
1491 'SG' => 'en-SG',
1492 'SK' => 'sk-SK',
1493 'SI' => 'sl-SI',
1494 'SB' => 'en-SB',
1495 'SO' => 'so-SO',
1496 'ZA' => 'en-ZA',
1497 'ES' => 'es-ES',
1498 'LK' => 'si-LK',
1499 'SD' => 'ar-SD',
1500 'SR' => 'nl-SR',
1501 'SZ' => 'en-SZ',
1502 'SE' => 'sv-SE',
1503 'CH' => 'de-CH',
1504 'SY' => 'ar-SY',
1505 'TW' => 'zh-TW',
1506 'TJ' => 'tg-TJ',
1507 'TZ' => 'sw-TZ',
1508 'TH' => 'th-TH',
1509 'TL' => 'pt-TL',
1510 'TG' => 'fr-TG',
1511 'TO' => 'to-TO',
1512 'TT' => 'en-TT',
1513 'TN' => 'ar-TN',
1514 'TR' => 'tr-TR',
1515 'TM' => 'tk-TM',
1516 'TV' => 'en-TV',
1517 'UG' => 'en-UG',
1518 'UA' => 'uk-UA',
1519 'AE' => 'ar-AE',
1520 'GB' => 'en-GB',
1521 'US' => 'en-US',
1522 'UY' => 'es-UY',
1523 'UZ' => 'uz-UZ',
1524 'VU' => 'bi-VU',
1525 'VE' => 'es-VE',
1526 'VN' => 'vi-VN',
1527 'YE' => 'ar-YE',
1528 'ZM' => 'en-ZM',
1529 'ZW' => 'en-ZW'
1530 ];
1531 }
1532
1533 /**
1534 * Returns a translatable string with a shortcode inserted in the correct format.
1535 *
1536 * @param string $shortcode The shortcode to be inserted (e.g., '[fluent_cart_receipt]').
1537 * @return string The formatted translatable string.
1538 */
1539 public static function getShortcodeInstructionString(string $shortcode, $pageName = ''): string
1540 {
1541 $copyToClipboard = __('Copy to clipboard', 'fluent-cart');
1542 return sprintf(
1543 /* translators: %s: Shortcode */
1544 '<p>' . _x('Use %1$s shortcode in your page.', 'Shortcode instruction message', 'fluent-cart') . '</p>',
1545 '<code class="copyable-content" title="' . $copyToClipboard . '">' . ($shortcode) . '</code>',
1546 //$pageName
1547 );
1548 }
1549
1550
1551 /**
1552 * Get the current user Model.
1553 * @return User|\FluentCart\Framework\Database\Orm\Builder|\FluentCart\Framework\Database\Orm\Builder[]|\FluentCart\Framework\Database\Orm\Collection|\FluentCart\Framework\Database\Orm\Model|null
1554 */
1555 public static function getCurrentUser($refresh = false)
1556 {
1557 static $user = false;
1558
1559 if (!$refresh && $user !== false) {
1560 return $user;
1561 }
1562
1563 $userId = get_current_user_id();
1564 if (!$userId) {
1565 $user = null;
1566 return $user;
1567 }
1568
1569 $user = User::query()->find($userId);
1570
1571 return $user;
1572
1573 }
1574
1575 public static function hasLicense($product): bool
1576 {
1577 if (empty($product)) {
1578 return false;
1579 }
1580
1581 $meta = Arr::get($product, 'licensesMeta.meta_value', []);
1582
1583 if (empty($meta)) {
1584 return false;
1585 }
1586
1587 $meta = is_string($meta) ? json_decode($meta, true) : $meta;
1588
1589 return Arr::get($meta, 'enabled') === 'yes';
1590
1591 }
1592
1593
1594 public static function generateDownloadFileLink($productDownload, $orderId = null, $validityInMinutes = 60, $isAdmin = false): string
1595 {
1596 $identifier = Arr::get($productDownload, 'download_identifier', '');
1597
1598 $validityInMinutes = apply_filters('fluent_cart/download_link_validity_in_minutes', $validityInMinutes, [
1599 'product_download' => $productDownload,
1600 'order_id' => $orderId,
1601 'is_admin' => $isAdmin,
1602 ]);
1603
1604 $signParams = [
1605 'download_identifier' => $identifier,
1606 'valid_till' => DateTime::now()
1607 ->addMinutes($validityInMinutes ?? 60)
1608 ->getTimestamp()
1609 ];
1610
1611 if ($orderId) {
1612 $orderId = Arr::wrap($orderId);
1613 $signParams['order_id'] = json_encode($orderId);
1614 }
1615
1616 $url = (new BaseUrl())->sign(site_url('/'), $signParams);
1617
1618 return URL::appendQueryParams($url, [
1619 'fluent-cart' => 'download-by-id',
1620 ]);
1621
1622 }
1623
1624
1625 public static function readableFileSize($bytes): string
1626 {
1627 // Converts bytes to a human-readable format (e.g., KB, MB, GB)
1628 // Example: 1024 -> "1 KB"
1629 // Example: 1048576 -> "1 MB"
1630
1631 if (!$bytes && $bytes !== 0) return '';
1632 $units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
1633 $i = floor(log($bytes, 2) / 10);
1634 $size = $bytes / pow(1024, $i);
1635 return round($size, 2) . ' ' . $units[$i];
1636 }
1637
1638
1639 public static function getSitePrefix()
1640 {
1641 $siteUrl = rtrim(home_url(), '/');
1642 // remove http:// or https:// from the URL
1643 $siteUrl = preg_replace('#^https?://#', '', $siteUrl);
1644 $sitePrefix = str_replace(['/', '.'], '_', $siteUrl);
1645
1646 return apply_filters('fluent_cart/site_prefix', $sitePrefix, []);
1647 }
1648
1649 public static function humanIntervalMaps($interval = '')
1650 {
1651 $intervals = [
1652 'daily' => __('day', 'fluent-cart'),
1653 'weekly' => __('week', 'fluent-cart'),
1654 'monthly' => __('month', 'fluent-cart'),
1655 'quarterly' => __('quarter', 'fluent-cart'),
1656 'half_yearly' => __('six month', 'fluent-cart'),
1657 'yearly' => __('year', 'fluent-cart'),
1658 ];
1659
1660 return Arr::get($intervals, $interval);
1661 }
1662
1663 /**
1664 * @return array Array of intervals with label and value
1665 */
1666 public static function getAvailableSubscriptionIntervalOptions(): array
1667 {
1668 $intervals = [
1669 [
1670 'label' => __('Yearly', 'fluent-cart'),
1671 'value' => 'yearly',
1672 'map_value' => 'year',
1673 ],
1674 [
1675 'label' => __('Half Yearly', 'fluent-cart'),
1676 'value' => 'half_yearly',
1677 'map_value' => 'half_year',
1678 ],
1679 [
1680 'label' => __('Quarterly', 'fluent-cart'),
1681 'value' => 'quarterly',
1682 'map_value' => 'quarter',
1683 ],
1684 [
1685 'label' => __('Monthly', 'fluent-cart'),
1686 'value' => 'monthly',
1687 'map_value' => 'month',
1688 ],
1689 [
1690 'label' => __('Weekly', 'fluent-cart'),
1691 'value' => 'weekly',
1692 'map_value' => 'week',
1693 ],
1694 [
1695 'label' => __('Daily', 'fluent-cart'),
1696 'value' => 'daily',
1697 'map_value' => 'day',
1698 ]
1699 ];
1700
1701 return apply_filters('fluent_cart/available_subscription_interval_options', $intervals);
1702 }
1703
1704 public static function translateIntervalToStandardFormat($repeatInterval)
1705 {
1706 if (empty($repeatInterval)) {
1707 return 'year';
1708 }
1709
1710 $intervalMaps = static::getAvailableSubscriptionIntervalMaps();
1711
1712 if (!isset($intervalMaps[$repeatInterval])) {
1713 return 'year';
1714 }
1715
1716 return $intervalMaps[$repeatInterval];
1717 }
1718
1719 public static function getAvailableSubscriptionIntervalMaps()
1720 {
1721 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
1722
1723 $intervalMaps = [];
1724 foreach ($intervalOptions as $option) {
1725 $intervalMaps[$option['value']] = $option['map_value'];
1726 }
1727
1728 return $intervalMaps;
1729
1730 }
1731
1732 public static function calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval)
1733 {
1734 $intervalInDays = static::subscriptionIntervalInDays($repeatInterval);
1735
1736 $maxTrialDaysAllowed = apply_filters('fluent_cart/max_trial_days_allowed', 365, [
1737 'existing_trial_days' => $trialDays,
1738 'repeat_interval' => $repeatInterval,
1739 'interval_in_days' => $intervalInDays,
1740 ]);
1741
1742 return min($trialDays + $intervalInDays, $maxTrialDaysAllowed); // return the minimum of the sum of the existing trial days and the interval days, and the max trial allowed
1743
1744 }
1745
1746 public static function subscriptionIntervalInDays($interval)
1747 {
1748 switch ($interval) {
1749 case 'daily':
1750 return 1;
1751 case 'weekly':
1752 return 7;
1753 case 'monthly':
1754 return 30;
1755 case 'quarterly':
1756 return 90;
1757 case 'half_yearly':
1758 return 182;
1759 case 'yearly':
1760 return 365;
1761 default:
1762 return apply_filters('fluent_cart/subscription_interval_in_days', 0, [
1763 'interval' => $interval,
1764 ]);
1765 }
1766 }
1767
1768 public static function parseTermIdsForFilter($filters): array
1769 {
1770 $taxonomies = Taxonomy::getTaxonomies();
1771 if (is_string($filters)) {
1772 $filters = json_decode($filters, true);
1773 }
1774 $formattedFilters = [];
1775
1776 foreach ($taxonomies as $key => $taxonomy) {
1777 $terms = Arr::get($filters, $key, []);
1778 if (!is_array($terms)) {
1779 $terms = [$terms];
1780 }
1781
1782 $terms = array_filter($terms, function ($term) {
1783 return !empty($term);
1784 });
1785
1786 if (!empty($terms)) {
1787 $terms = array_map(function ($term) {
1788 return sanitize_text_field((string)$term);
1789 }, $terms);
1790
1791
1792 $formattedFilters[$key] = $terms;
1793 }
1794
1795
1796 }
1797
1798 return $formattedFilters;
1799 }
1800
1801 public static function mergeTermIdsForFilter($array1 = [], $array2 = []): array
1802 {
1803 $result = [];
1804
1805 foreach ([$array1, $array2] as $array) {
1806 foreach ($array as $key => $values) {
1807 if (!isset($result[$key])) {
1808 $result[$key] = [];
1809 }
1810 $result[$key] = array_values(array_unique(array_merge($result[$key], $values)));
1811 }
1812 }
1813
1814 return $result;
1815 }
1816
1817 public static function loadBundleChild(array $variants, $select = ['id', 'variation_title']): array
1818 {
1819 $allChildVariants = Arr::pluck($variants, 'other_info.bundle_child_ids');
1820
1821 $allChildVariants = array_unique(Arr::flatten($allChildVariants));
1822 $allChildVariants = Arr::except(
1823 $allChildVariants,
1824 Arr::pluck($variants, 'id')
1825 );
1826 $allChildVariants = array_filter($allChildVariants);
1827 $childVariants = ProductVariation::query()
1828 ->whereIn('id', $allChildVariants)
1829 ->with('product:ID,post_title')
1830 ->select($select)
1831 ->get()
1832 ->toArray();
1833
1834 // Extract post_title from product and remove product object to keep data clean
1835 foreach ($childVariants as $key => $childVariant) {
1836 $postTitle = Arr::get($childVariant, 'product.post_title');
1837 if ($postTitle) {
1838 $childVariants[$key]['post_title'] = $postTitle;
1839 unset($childVariants[$key]['product']);
1840 }
1841 }
1842
1843 foreach ($variants as &$variant) {
1844 $childIds = Arr::get($variant, 'other_info.bundle_child_ids', []);
1845 $variant['bundle_child_ids'] = $childIds;
1846 if (count($childIds) < 1) {
1847 $variant['child_variants'] = [];
1848 continue;
1849 }
1850 foreach ($childVariants as $childVariant) {
1851 if (in_array($childVariant['id'], Arr::get($variant, 'other_info.bundle_child_ids', []))) {
1852 $variant['child_variants'][$childVariant['id']] = $childVariant;
1853 }
1854 }
1855 }
1856
1857 return $variants;
1858 }
1859
1860 /**
1861 * Translate digits in a number according to the configured numeric system.
1862 *
1863 * Converts the digits 0-9 in the given number to their equivalents
1864 * defined in the numeric system string from `dateTimeStrings()`.
1865 * This allows displaying numbers in other numeral systems, e.g., Bengali, Arabic, Hindi, etc.
1866 *
1867 * Only digits are translated; other characters such as decimal points, currency symbols, or text remain unchanged.
1868 *
1869 * @param int|float|string $number The number to translate.
1870 * @return string The number with digits translated according to the configured numeric system.
1871 */
1872 public static function translateNumber($number): string
1873 {
1874 $config = TransStrings::dateTimeStrings();
1875 $numericSystem = Arr::get($config, 'numericSystem', '0_1_2_3_4_5_6_7_8_9');
1876 $digits = explode('_', $numericSystem);
1877
1878 return strtr(
1879 (string)$number,
1880 array_combine(range(0, 9),
1881 $digits)
1882 );
1883 }
1884
1885 public static function isModalCheckoutEnabled(): bool
1886 {
1887 //$storeSettings = new StoreSettings();
1888 //$enableModalCheckout = $storeSettings->get('enable_modal_checkout', 'no');
1889 return apply_filters('fluent_cart/enable_modal_checkout', false);
1890 }
1891
1892 public static function isAdminUser(): bool
1893 {
1894 return current_user_can('manage_options');
1895 }
1896
1897 /**
1898 * Convert string/boolean to actual boolean value.
1899 * Handles shortcode string attributes like "true"/"false"
1900 *
1901 * @param mixed $value The value to convert to boolean
1902 * @param bool $default Default value if conversion fails
1903 * @return bool The boolean result
1904 */
1905 public static function toBool($value, bool $default = false): bool
1906 {
1907 if (is_bool($value)) {
1908 return $value;
1909 }
1910
1911 if (is_string($value)) {
1912 $value = strtolower(trim($value));
1913 if (in_array($value, ['true', '1', 'yes', 'on'], true)) {
1914 return true;
1915 }
1916 if (in_array($value, ['false', '0', 'no', 'off'], true)) {
1917 return false;
1918 }
1919 }
1920
1921 return $default;
1922 }
1923
1924 public static function formatTaxRatePercent(float $rate): string
1925 {
1926 $formatted = number_format($rate, 4, '.', '');
1927 if (strpos($formatted, '.') !== false) {
1928 $formatted = rtrim($formatted, '0');
1929 $formatted = rtrim($formatted, '.');
1930 }
1931 return $formatted;
1932 }
1933
1934 /**
1935 * Returns the tax label for order-level tax rows (tax_total, not per-item).
1936 * For mixed orders, indicates tax varies per item.
1937 *
1938 * @param \FluentCart\App\Models\Order $order
1939 * @return string
1940 */
1941 public static function getOrderTaxLabel($order) {
1942 if ((int) $order->tax_behavior === 3) {
1943 return esc_html__('(Varies)', 'fluent-cart');
1944 }
1945 return (int) $order->tax_behavior === 2
1946 ? esc_html__('(Included)', 'fluent-cart')
1947 : esc_html__('(Excluded)', 'fluent-cart');
1948 }
1949 }
1950