PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.1
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.1
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 trunk All 48 releases
fluent-cart / app / Helpers / Helper.php

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

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