PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.0
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.6.0, at app/Helpers/Helper.php

1,988 lines 65.8 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 public static function toDecimalWithoutComma($amount)
583 {
584
585 if (!is_numeric($amount)) {
586 return 0;
587 }
588
589 // Convert to float and divide by 100
590 $result = floatval($amount) / 100;
591
592 // Ensure exactly two decimal places
593 return round($result, 2);
594 }
595
596 public static function getCustomerByUser($user)
597 {
598 if (is_numeric($user)) {
599 $user = get_user_by('ID', $user);
600 }
601
602 if (!$user) {
603 return null;
604 }
605
606 return Customer::query()->where('user_id', $user->ID)
607 ->orWhere('email', $user->user_email)
608 ->first();
609 }
610
611 /**
612 * @param array $order_data
613 *
614 * @return array return ['billing_address','shipping_address','others'];
615 */
616
617 /**
618 *
619 * @return string
620 */
621 public static function getProductImageBaseUri(): string
622 {
623 $uploads = wp_upload_dir();
624
625 return $uploads['baseurl'] . '/' . FLUENTCART_UPLOAD_DIR . '/product_image/';
626 }
627
628 public static function getProductImageBaseDir()
629 {
630 $uploads = wp_upload_dir();
631
632 return $uploads['basedir'] . '/' . FLUENTCART_UPLOAD_DIR . '/product_image/';
633 }
634
635 public static function getAvailableCurrencyList()
636 {
637 $currencies = apply_filters_deprecated('fluent-cart/available_currencies', [
638 [
639 'BDT' => [
640 "label" => __('Bangladeshi Taka', 'fluent-cart'),
641 "value" => 'BDT',
642 "symbol" => '৳',
643 ],
644 'USD' => [
645 "label" => __('United State Dollar', 'fluent-cart'),
646 "value" => 'USD',
647 "symbol" => '$',
648 ],
649 'GBP' => [
650 "label" => __('United Kingdom', 'fluent-cart'),
651 "value" => 'GBP',
652 "symbol" => '£',
653 ],
654 ], []
655 ], '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.');
656
657 return apply_filters('fluent_cart/available_currencies', $currencies, []);
658 }
659
660 public static function getSymbolForCurrency($currency = 'BDT')
661 {
662
663 $symbol = '৳';
664 $list = self::getAvailableCurrencyList();
665
666 return $list[$currency]['symbol'] ?? $symbol;
667 }
668
669 public function getConfirmationSettings()
670 {
671 return (new Confirmation())->get();
672 }
673
674
675 /**
676 *
677 * @param $remove
678 * @return string
679 */
680 public static function getCheckoutPageLinkAfterRemovingGetParams($remove = [])
681 {
682
683 global $fct_store;
684
685 $link = Arr::get($fct_store, 'checkout_link');
686
687 $link .= '?';
688
689 if (!empty($remove)) {
690 $params = App::request()->all();
691
692 foreach ($params as $key => $val) {
693
694 if (!in_array($key, $remove)) {
695
696 $link .= $key . '=' . $val . '&';
697 }
698 }
699 }
700
701 return rtrim($link, '&');
702 }
703
704
705 /**
706 *
707 * @return bool
708 */
709 public static function isSingleProductPage(): bool
710 {
711 return is_singular([FluentProducts::CPT_NAME]);
712 }
713
714
715 public static function isTrue($array, $key)
716 {
717 $value = $array[$key] ?? false;
718 if (is_bool($value)) {
719 return $value;
720 }
721 if ($value === 'false' || !$value) {
722 return false;
723 }
724 return true;
725 }
726
727 public static function is_valid_json($string): bool
728 {
729 if (!is_string($string)) {
730 return false;
731 }
732
733 $trimmed = trim($string);
734
735 // Basic check: must start with { or [ and end with } or ]
736 if (!preg_match('/^(\{.*\}|\[.*\])$/s', $trimmed)) {
737 return false;
738 }
739
740 json_decode($trimmed);
741 return json_last_error() === JSON_ERROR_NONE;
742 }
743
744 public static function getStockStatuses($withLabel = true)
745 {
746 $statues = [
747 'in-stock' => __('In Stock', 'fluent-cart'),
748 'out-of-stock' => __('Out Of Stock', 'fluent-cart'),
749 ];
750
751 if ($withLabel) {
752 return $statues;
753 }
754
755 return array_keys($statues);
756 }
757
758 public static function getFulfilmentTypes($withLabel = true)
759 {
760 $statues = [
761 'physical' => __('Physical', 'fluent-cart'),
762 'digital' => __('Digital', 'fluent-cart'),
763 ];
764
765 if ($withLabel) {
766 return $statues;
767 }
768
769 return array_keys($statues);
770 }
771
772 public static function getVariationTypes($withLabel = true)
773 {
774 // advanced_variations is advertised in free too so the variation-type
775 // dropdown can offer it (shown Pro-locked with a crown and an upgrade
776 // modal while Pro is inactive). Pro re-registers the same key via the
777 // filter below when active.
778 $types = [
779 'simple' => __('Simple', 'fluent-cart'),
780 'simple_variations' => __('Simple Variations', 'fluent-cart'),
781 'advanced_variations' => __('Advanced Variations', 'fluent-cart'),
782 ];
783
784 $types = apply_filters('fluent_cart/variation_types', $types);
785
786 if ($withLabel) {
787 return $types;
788 }
789
790 return array_keys($types);
791 }
792
793 public static function isValueEncrypted($raw_value)
794 {
795 if (!$raw_value || !is_string($raw_value) || !extension_loaded('openssl')) {
796 return false;
797 }
798
799 // Check if input is valid base64
800 if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $raw_value)) {
801 return false;
802 }
803
804 $decoded = base64_decode($raw_value, true);
805 if ($decoded === false) {
806 return false;
807 }
808
809 // Check if decoded string is long enough for IV
810 $method = 'aes-256-ctr';
811 $ivlen = openssl_cipher_iv_length($method);
812 if (strlen($decoded) < $ivlen) {
813 return false;
814 }
815
816 $iv = substr($decoded, 0, $ivlen);
817 $ciphertext = substr($decoded, $ivlen);
818
819 $key = (defined('FLUENT_CART_ENCRYPTION_KEY'))
820 ? FLUENT_CART_ENCRYPTION_KEY
821 : ((defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY)
822 ? LOGGED_IN_KEY
823 : 'this-is-a-fallback-key-but-not-secure');
824 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT)
825 ? LOGGED_IN_SALT
826 : 'this-is-a-fallback-salt-but-not-secure';
827
828 $value = openssl_decrypt($ciphertext, $method, $key, 0, $iv);
829 if ($value === false) {
830 return false;
831 }
832
833
834 return substr($value, -strlen($salt)) === $salt;
835 }
836
837 public static function encryptKey($value)
838 {
839 if (!$value) {
840 return $value;
841 }
842
843 if (!extension_loaded('openssl')) {
844 return $value;
845 }
846
847 if (self::isValueEncrypted($value)) {
848 return $value;
849 }
850
851 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
852
853 if (defined('FLUENT_CART_ENCRYPTION_KEY')) {
854 $key = FLUENT_CART_ENCRYPTION_KEY;
855 } else {
856 $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
857 }
858
859 $method = 'aes-256-ctr';
860 $ivlen = openssl_cipher_iv_length($method);
861 $iv = openssl_random_pseudo_bytes($ivlen);
862
863 $raw_value = openssl_encrypt($value . $salt, $method, $key, 0, $iv);
864 if (!$raw_value) {
865 return false;
866 }
867
868 return base64_encode($iv . $raw_value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
869 }
870
871 public static function decryptKey($raw_value)
872 {
873
874 if (!$raw_value) {
875 return $raw_value;
876 }
877
878 if (!extension_loaded('openssl')) {
879 return $raw_value;
880 }
881
882 $decoded = base64_decode($raw_value, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
883
884 if ($decoded === false) {
885 return $raw_value;
886 }
887
888 $method = 'aes-256-ctr';
889 $ivlen = openssl_cipher_iv_length($method);
890
891 if (strlen($decoded) <= $ivlen) {
892 return $raw_value;
893 }
894
895 $iv = substr($decoded, 0, $ivlen);
896
897 $ciphertext = substr($decoded, $ivlen);
898
899 if (defined('FLUENT_CART_ENCRYPTION_KEY')) {
900 $key = FLUENT_CART_ENCRYPTION_KEY;
901 } else {
902 $key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
903 }
904
905 $salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
906
907 $decrypted = openssl_decrypt($ciphertext, $method, $key, 0, $iv);
908 if (!$decrypted || substr($decrypted, -strlen($salt)) !== $salt) {
909 return false;
910 }
911
912 return substr($decrypted, 0, -strlen($salt));
913 }
914
915 /**
916 * @return array
917 * For kses_post to allow some html tags
918 */
919 public static function allowedHTMLForCheckout(): array
920 {
921
922 $allowedTags = [
923 'svg' => [
924 'class' => true,
925 'aria-hidden' => true,
926 'aria-labelledby' => true,
927 'role' => true,
928 'xmlns' => true,
929 'width' => true,
930 'height' => true,
931 'viewbox' => true,
932 'fill' => true
933 ],
934 'g' => [
935 'fill' => true
936 ],
937 'title' => ['title' => true],
938 'path' => [
939 'd' => true,
940 'fill' => true,
941 'stroke' => true,
942 'fill-rule' => true,
943 'clip-rule' => true,
944 ],
945 ];
946
947 foreach (['input', 'label', 'div', 'span', 'p', 'select', 'option', 'textarea', 'button', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as $tag) {
948 $allowedTags[$tag] = [
949 'selected',
950 'type' => [],
951 "selected='selected'" => [],
952 'name' => [],
953 'value' => [],
954 'autocomplete' => [],
955 'placeholder' => [],
956 'data-required' => [],
957 'data-type' => [],
958 'id' => [],
959 'class' => [],
960 'required' => [],
961 'disabled' => [],
962 'for' => [],
963 'style' => [],
964 'checked' => [],
965 'maxlength' => [],
966 'data-id' => [],
967 'data-country' => [],
968 'data-fluent-cart-checkout-page-form-address-modal-open-button' => [],
969 'data-fluent-cart-checkout-page-form-address-modal-wrapper' => [],
970 'data-fluent-cart-checkout-page-form-input-wrapper' => [],
971 'data-fluent_cart_checkout_error' => [],
972 'data-fluent-cart-checkout-page-form-address-modal-body' => [],
973 'data-fluent-cart-checkout-page-form-address-modal-address-selector-button' => [],
974 'data-fluent-cart-checkout-page-form-address-input' => [],
975 'data-fluent-cart-checkout-page-form-address-select-wrapper' => [],
976 'data-fluent-cart-checkout-page-form-address-modal-close-button' => [],
977 'data-fluent-cart-checkout-page-form-address-modal-address-selector-button-wrapper' => [],
978 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-button' => [],
979 'data-fluent-cart-checkout-page-form-address-modal-apply-button' => [],
980 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-form-wrapper' => [],
981 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-submit-button' => [],
982 'data-fluent-cart-checkout-page-form-address-show-add-new-modal-cancel-button' => [],
983 'data-fluent-cart-address-type' => [],
984 'data-fluent-cart-checkout-page-form-address-info-wrapper' => [],
985 'data-fluent-cart-checkout-page-form-error' => [],
986 'data-fluent-cart-checkout-page-form-section' => [],
987 'data-fluent-cart-checkout-page-discount-container' => [],
988 'data-fluent-cart-checkout-page-final-amount-container' => [],
989 'data-fluent-cart-checkout-page-new-total-amount' => [],
990 'data-fluent-cart-checkout-page-final-amount' => [],
991 'data-fluent-cart-checkout-page-coupon-validate' => [],
992 'data-fluent-cart-checkout-coupon-items-toggle' => [],
993 'data-fluent-cart-checkout-coupon-items-wrapper' => []
994
995
996 ];
997 }
998
999 return $allowedTags;
1000 }
1001
1002 public static function getCouponStatuses()
1003 {
1004 $statuses = apply_filters_deprecated('fluent-cart/coupon_statuses', [
1005 [
1006 'active' => __('Active', 'fluent-cart'),
1007 'expired' => __('Expired', 'fluent-cart'),
1008 'disabled' => __('Disabled', 'fluent-cart'),
1009 ], []
1010 ], '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.');
1011
1012 return apply_filters('fluent_cart/coupon_statuses', $statuses, []);
1013 }
1014
1015 public static function getCouponSuccessStatuses()
1016 {
1017 return [
1018 'active'
1019 ];
1020 }
1021
1022
1023 /**
1024 * Compact subscription terms as text
1025 *
1026 * Expected $data keys:
1027 * - trial_days (int)
1028 * - interval ('daily'|'weekly'|'monthly'|'yearly')
1029 * - interval_count (int, default 1)
1030 * - times (int; 0 = open-ended, >0 = finite number of payments/cycles)
1031 * - price (string; formatted with currency)
1032 * - signup_fee (string; 0 if none)
1033 * - compare_price (string; 0 if none)
1034 *
1035 * Output examples:
1036 * - "30 days free then $100.00 per year"
1037 * - "$100 per year + $10 one-time signup fee"
1038 * - "$100/month for 4 months"
1039 * - "30 days free then $100/month for 4 months"
1040 * - "$99 per month"
1041 */
1042 public static function getSubscriptionTermText(array $data, $asHtml = false): string
1043 {
1044 // Fixed interval options
1045 $intervalOptions = static::getAvailableSubscriptionIntervalMaps();
1046
1047 // Normalize / defaults
1048 $trialDays = $data['trial_days'] ?? 0;
1049 $interval = (string)($data['interval'] ? $data['interval'] : 'monthly');
1050
1051 $unit = '';
1052 if (isset($intervalOptions[$interval])) {
1053 $unit = $intervalOptions[$interval];
1054 } else if ($interval) {
1055 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
1056 foreach ($intervalOptions as $option) {
1057 if ($option['value'] === $interval) {
1058 $unit = strtolower($option['label']);
1059 break;
1060 }
1061 }
1062
1063 if (!$unit) {
1064 $unit = strtolower(str_replace(['_', '-'], ' ', $interval));
1065 }
1066 }
1067
1068 if (!$unit) {
1069 $unit = 'year';
1070 }
1071
1072 $count = max(1, (int)($data['interval_count'] ?? 1));
1073 $times = (int)($data['times'] ?? 0);
1074 $price = (string)($data['price'] ?? '');
1075 $signupFee = $data['signup_fee'] ?? '';
1076 $compare = $data['compare_price'] ?? '';
1077
1078 $signupFeeLabel = $data['signup_fee_label'] ?? __('signup fee', 'fluent-cart');
1079
1080 // helpers
1081 $pluralUnit = static function (string $unit, int $n): string {
1082 switch ($unit) {
1083 case 'day':
1084 return _n('day', 'days', $n, 'fluent-cart');
1085 case 'week':
1086 return _n('week', 'weeks', $n, 'fluent-cart');
1087 case 'quarter':
1088 return _n('quarter', 'quarters', $n, 'fluent-cart');
1089 case 'half_year':
1090 return _n('six month', 'six months', $n, 'fluent-cart');
1091 case 'year':
1092 return _n('year', 'years', $n, 'fluent-cart');
1093 case 'month':
1094 return _n('month', 'months', $n, 'fluent-cart');
1095 default:
1096 // For custom intervals, add 's' for plural
1097 return ($n > 1) ? $unit . 's' : $unit;
1098 }
1099 };
1100
1101 // Build the “per …” phrase (e.g., "per month", "per 3 months")
1102 $perPhrase = ($count === 1)
1103 ? sprintf(
1104 /* translators: %s is the singular unit name (e.g., day, month, year) */
1105 __('per %s', 'fluent-cart'),
1106 $pluralUnit($unit, 1)
1107 )
1108 : sprintf(
1109 /* translators: %1$d is the count number, %2$s is the plural unit name (e.g., days, months, years) */
1110 __('per %1$d %2$s', 'fluent-cart'),
1111 $count,
1112 $pluralUnit($unit, $count)
1113 );
1114
1115 // Main price phrase:
1116 // - If monthly installments (times > 0 and unit == month and count == 1): "$X/month for N months"
1117 // - Else if times > 0: "$X per {count unit(s)} for N {cycle(s)}"
1118 // - Else: "$X per {count unit(s)}"
1119 if ($times > 0) {
1120 if ($unit === 'month' && $count === 1) {
1121 $installmentTail = sprintf(
1122 /* translators: %s: pluralized 'month(s)' */
1123 __('for %1$d %2$s', 'fluent-cart'),
1124 $times,
1125 $pluralUnit('month', $times)
1126 );
1127 $main = sprintf(
1128 /* translators: Compact monthly installment, e.g. "$100/month for 4 months" */
1129 __('%1$s/%2$s %3$s', 'fluent-cart'),
1130 $price,
1131 __('month', 'fluent-cart'),
1132 $installmentTail
1133 );
1134 } else {
1135 // Generic installment wording: "for N cycle(s)"
1136 $cyclesTail = sprintf(
1137 /* translators: %1$d is the number of cycles, %2$s is "cycle" or "cycles" */
1138 __('for %1$d %2$s', 'fluent-cart'),
1139 $times,
1140 _n('cycle', 'cycles', $times, 'fluent-cart')
1141 );
1142 $main = sprintf(
1143 /* translators: e.g. "$100 per 3 months for 4 cycles" */
1144 __('%1$s %2$s %3$s', 'fluent-cart'),
1145 $price,
1146 $perPhrase,
1147 $cyclesTail
1148 );
1149 }
1150 } else {
1151 // Open-ended subscription
1152 $main = sprintf(
1153 /* translators: e.g. "$99 per month" */
1154 __('%1$s %2$s', 'fluent-cart'),
1155 $price,
1156 $perPhrase
1157 );
1158 }
1159
1160 // Prefix trial if present: "N days free then …"
1161 if ($trialDays > 0) {
1162 $trialFrag = sprintf(
1163 /* translators: "30 days free then" */
1164 __('%1$d %2$s free then', 'fluent-cart'),
1165 $trialDays,
1166 $pluralUnit('day', $trialDays)
1167 );
1168 $main = $trialFrag . ' ' . $main;
1169 }
1170
1171 // Append signup fee if any: " + $10 one-time signup fee"
1172 if ($signupFee) {
1173 $main .= ' ' . sprintf(
1174 /* translators: e.g. "+ $10 one-time signup fee" */
1175 __('+ %1$s one-time %2$s', 'fluent-cart'),
1176 $signupFee,
1177 $signupFeeLabel
1178 );
1179 }
1180
1181 // No trailing period (matches your examples)
1182 return $main;
1183 }
1184
1185 public static function getTranslatedIntervalUnit(string $unit): string
1186 {
1187 switch ($unit) {
1188 case 'day':
1189 return __('day', 'fluent-cart');
1190 case 'week':
1191 return __('week', 'fluent-cart');
1192 case 'month':
1193 return __('month', 'fluent-cart');
1194 case 'quarter':
1195 return __('quarter', 'fluent-cart');
1196 case 'half_year':
1197 return __('six month', 'fluent-cart');
1198 case 'year':
1199 return __('year', 'fluent-cart');
1200 default:
1201 return $unit;
1202 }
1203 }
1204
1205 public static function generateSubscriptionInfo($otherInfo, $itemPrice, $currencyCode = null): ?string
1206 {
1207 // Convert to array only if it's an object
1208 if (is_object($otherInfo)) {
1209 $otherInfo = json_decode(json_encode($otherInfo), true);
1210 }
1211
1212 $price = self::toDecimal($itemPrice, true, $currencyCode);
1213 $recurringDiscountAmount = Arr::get($otherInfo, 'recurring_discounts.amount', 0);
1214
1215 if ($recurringDiscountAmount) {
1216 $newRecurringAmount = $itemPrice - $recurringDiscountAmount;
1217 $price = "<del>" . $price . "</del> " . self::toDecimal($newRecurringAmount, true, $currencyCode);
1218 }
1219
1220 $repeatInterval = Arr::get($otherInfo, 'repeat_interval', '');
1221 $occurrence = (int)Arr::get($otherInfo, 'times', 0);
1222
1223 $intervalOptions = static::getAvailableSubscriptionIntervalMaps();
1224
1225 $intervalUnit = '';
1226 if (isset($intervalOptions[$repeatInterval])) {
1227 $intervalUnit = $intervalOptions[$repeatInterval];
1228 } else if ($repeatInterval) {
1229 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
1230 foreach ($intervalOptions as $option) {
1231 if ($option['value'] === $repeatInterval) {
1232 $intervalUnit = strtolower($option['label']);
1233 break;
1234 }
1235 }
1236
1237 if (!$intervalUnit) {
1238 $intervalUnit = ucwords(str_replace(['_', '-'], ' ', $repeatInterval));
1239 }
1240 }
1241
1242 $intervalLabel = Helper::getTranslatedIntervalUnit($intervalUnit);
1243
1244 $interval = $intervalUnit
1245 ? sprintf(
1246 /* translators: %s is the interval (e.g., day, week, month, quarter, half_year, year) */
1247 __('per %s', 'fluent-cart'),
1248 $intervalLabel
1249 )
1250 : '';
1251
1252 $time = $intervalUnit
1253 ? $intervalLabel
1254 : '';
1255
1256 $paymentInfo = sprintf(
1257 /* 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 */
1258 __('%1$s %2$s, for %3$s %4$s', 'fluent-cart'),
1259 $price,
1260 $interval,
1261 $occurrence,
1262 $time
1263 );
1264
1265 if (empty($occurrence)) {
1266 $paymentInfo = sprintf(
1267 /* translators: %1$s is the price, %2$s is the interval, %3$s is "until cancel" text */
1268 __('%1$s %2$s %3$s', 'fluent-cart'),
1269 $price,
1270 $interval,
1271 __('until cancel', 'fluent-cart')
1272 );
1273 }
1274
1275 return !empty($otherInfo) ? $paymentInfo : null;
1276 }
1277
1278 public static function generateSetupFeeInfo($otherInfo, $asArray = false)
1279 {
1280 // Convert to array if it's an object
1281 if (is_object($otherInfo)) {
1282 $otherInfo = json_decode(json_encode($otherInfo), true);
1283 }
1284
1285 $signupFeeName = Arr::get($otherInfo, 'signup_fee_name', __('Setup Fee', 'fluent-cart'));
1286 $fee = Arr::get($otherInfo, 'signup_fee', 0);
1287 $manageSetupFee = Arr::get($otherInfo, 'manage_setup_fee', 'no');
1288
1289 if ($manageSetupFee !== 'yes' || !$fee) {
1290 return '';
1291 }
1292
1293
1294 if ($originalSetupFee = Arr::get($otherInfo, 'original_signup_fee', 0)) {
1295 if ($fee != $originalSetupFee) {
1296 $title = __('Adjusted setup fee', 'fluent-cart');
1297 $formattedAmount = CurrencySettings::getPriceHtml($fee, null, true, true);
1298
1299 if ($asArray) {
1300 return [
1301 'signup_fee_name' => $title,
1302 'signup_fee' => $fee,
1303 'signup_fee_formatted' => $formattedAmount,
1304 ];
1305 }
1306 return $title . $formattedAmount;
1307 }
1308 }
1309
1310 $formattedAmount = CurrencySettings::getPriceHtml($fee, null, true, true);
1311 if ($asArray) {
1312 return [
1313 'signup_fee_name' => $signupFeeName,
1314 'signup_fee' => $fee,
1315 'signup_fee_formatted' => $formattedAmount,
1316 ];
1317 }
1318
1319
1320 return $signupFeeName . ' ' . $formattedAmount;
1321 }
1322
1323 public static function generateTrialInfo($otherInfo)
1324 {
1325 $trialInfo = '';
1326
1327 $trialDays = Arr::get($otherInfo, 'trial_days', 0);
1328
1329 if ($trialDays && Arr::get($otherInfo, 'is_trial_days_simulated', 'no') !== 'yes') {
1330 $trialInfo = sprintf(
1331 /* translators: %d is the number of trial days */
1332 __('Free Trial: %d days', 'fluent-cart'),
1333 $trialDays
1334 );
1335 }
1336
1337 return apply_filters('fluent_cart/trial_info', $trialInfo, $otherInfo);
1338 }
1339
1340
1341 public static function getCountryList(): array
1342 {
1343 $options = App::getInstance('localization')->countriesOptions();
1344 $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.');
1345
1346 return apply_filters('fluent_cart/util/countries', $options, []);
1347 }
1348
1349 public static function getCountyIsoLists(): array
1350 {
1351 return App::getInstance('localization')->getCountyIsoLists();
1352 }
1353
1354 public static function getCountryCode($country_name)
1355 {
1356 $countries = self::getCountryList();
1357 foreach ($countries as $country) {
1358 if ($country['name'] === $country_name) {
1359 return $country['value'];
1360 }
1361 }
1362 return '';
1363 }
1364
1365 /**
1366 * Get the country's name with country code,
1367 *
1368 * @param $code
1369 * @return string
1370 */
1371 public static function getCountryName($code): string
1372 {
1373 if (!$code || !is_string($code)) {
1374 return '';
1375 }
1376
1377 $countries = self::getCountyIsoLists();
1378
1379 return $countries[$code] ?? $code;
1380 }
1381
1382 public static function languageCodes(): array
1383 {
1384 return $langCodes = [
1385 'AF' => 'fa-AF',
1386 'AL' => 'sq-AL',
1387 'DZ' => 'ar-DZ',
1388 'AS' => 'sm-AS',
1389 'AD' => 'ca-AD',
1390 'AO' => 'pt-AO',
1391 'AR' => 'es-AR',
1392 'AM' => 'hy-AM',
1393 'AU' => 'en-AU',
1394 'AT' => 'de-AT',
1395 'AZ' => 'az-AZ',
1396 'BH' => 'ar-BH',
1397 'BD' => 'bn-BD',
1398 'BY' => 'be-BY',
1399 'BE' => 'nl-BE',
1400 'BZ' => 'en-BZ',
1401 'BJ' => 'fr-BJ',
1402 'BT' => 'dz-BT',
1403 'BO' => 'es-BO',
1404 'BA' => 'bs-BA',
1405 'BW' => 'en-BW',
1406 'BR' => 'pt-BR',
1407 'BN' => 'ms-BN',
1408 'BG' => 'bg-BG',
1409 'BF' => 'fr-BF',
1410 'BI' => 'fr-BI',
1411 'KH' => 'km-KH',
1412 'CM' => 'en-CM',
1413 'CA' => 'en-CA',
1414 'CV' => 'pt-CV',
1415 'CF' => 'fr-CF',
1416 'TD' => 'fr-TD',
1417 'CL' => 'es-CL',
1418 'CN' => 'zh-CN',
1419 'CO' => 'es-CO',
1420 'KM' => 'ar-KM',
1421 'CD' => 'fr-CD',
1422 'CG' => 'fr-CG',
1423 'CR' => 'es-CR',
1424 'CI' => 'fr-CI',
1425 'HR' => 'hr-HR',
1426 'CU' => 'es-CU',
1427 'CY' => 'el-CY',
1428 'CZ' => 'cs-CZ',
1429 'DK' => 'da-DK',
1430 'DJ' => 'fr-DJ',
1431 'DM' => 'en-DM',
1432 'DO' => 'es-DO',
1433 'EC' => 'es-EC',
1434 'EG' => 'ar-EG',
1435 'SV' => 'es-SV',
1436 'GQ' => 'es-GQ',
1437 'ER' => 'ti-ER',
1438 'EE' => 'et-EE',
1439 'ET' => 'am-ET',
1440 'FJ' => 'en-FJ',
1441 'FI' => 'fi-FI',
1442 'FR' => 'fr-FR',
1443 'GA' => 'fr-GA',
1444 'GM' => 'en-GM',
1445 'GE' => 'ka-GE',
1446 'DE' => 'de-DE',
1447 'GH' => 'en-GH',
1448 'GR' => 'el-GR',
1449 'GD' => 'en-GD',
1450 'GT' => 'es-GT',
1451 'GN' => 'fr-GN',
1452 'GW' => 'pt-GW',
1453 'GY' => 'en-GY',
1454 'HT' => 'fr-HT',
1455 'HN' => 'es-HN',
1456 'HU' => 'hu-HU',
1457 'IS' => 'is-IS',
1458 'IN' => 'hi-IN',
1459 'ID' => 'id-ID',
1460 'IR' => 'fa-IR',
1461 'IQ' => 'ar-IQ',
1462 'IE' => 'en-IE',
1463 'IL' => 'he-IL',
1464 'IT' => 'it-IT',
1465 'JM' => 'en-JM',
1466 'JP' => 'ja-JP',
1467 'JO' => 'ar-JO',
1468 'KZ' => 'kk-KZ',
1469 'KE' => 'sw-KE',
1470 'KI' => 'en-KI',
1471 'KR' => 'ko-KR',
1472 'KW' => 'ar-KW',
1473 'KG' => 'ky-KG',
1474 'LA' => 'lo-LA',
1475 'LV' => 'lv-LV',
1476 'LB' => 'ar-LB',
1477 'LS' => 'en-LS',
1478 'LR' => 'en-LR',
1479 'LY' => 'ar-LY',
1480 'LI' => 'de-LI',
1481 'LT' => 'lt-LT',
1482 'LU' => 'lb-LU',
1483 'MG' => 'mg-MG',
1484 'MW' => 'en-MW',
1485 'MY' => 'ms-MY',
1486 'MV' => 'dv-MV',
1487 'ML' => 'fr-ML',
1488 'MT' => 'mt-MT',
1489 'MH' => 'mh-MH',
1490 'MR' => 'ar-MR',
1491 'MU' => 'mfe-MU',
1492 'MX' => 'es-MX',
1493 'FM' => 'en-FM',
1494 'MD' => 'ro-MD',
1495 'MC' => 'fr-MC',
1496 'MN' => 'mn-MN',
1497 'ME' => 'sr-ME',
1498 'MA' => 'ar-MA',
1499 'MZ' => 'pt-MZ',
1500 'NA' => 'en-NA',
1501 'NR' => 'en-NR',
1502 'NP' => 'ne-NP',
1503 'NL' => 'nl-NL',
1504 'NZ' => 'en-NZ',
1505 'NI' => 'es-NI',
1506 'NG' => 'en-NG',
1507 'NO' => 'no-NO',
1508 'OM' => 'ar-OM',
1509 'PK' => 'ur-PK',
1510 'PW' => 'en-PW',
1511 'PA' => 'es-PA',
1512 'PG' => 'en-PG',
1513 'PY' => 'es-PY',
1514 'PE' => 'es-PE',
1515 'PH' => 'en-PH',
1516 'PL' => 'pl-PL',
1517 'PT' => 'pt-PT',
1518 'QA' => 'ar-QA',
1519 'RO' => 'ro-RO',
1520 'RU' => 'ru-RU',
1521 'RW' => 'rw-RW',
1522 'WS' => 'sm-WS',
1523 'SM' => 'it-SM',
1524 'SA' => 'ar-SA',
1525 'SN' => 'fr-SN',
1526 'RS' => 'sr-RS',
1527 'SC' => 'fr-SC',
1528 'SL' => 'en-SL',
1529 'SG' => 'en-SG',
1530 'SK' => 'sk-SK',
1531 'SI' => 'sl-SI',
1532 'SB' => 'en-SB',
1533 'SO' => 'so-SO',
1534 'ZA' => 'en-ZA',
1535 'ES' => 'es-ES',
1536 'LK' => 'si-LK',
1537 'SD' => 'ar-SD',
1538 'SR' => 'nl-SR',
1539 'SZ' => 'en-SZ',
1540 'SE' => 'sv-SE',
1541 'CH' => 'de-CH',
1542 'SY' => 'ar-SY',
1543 'TW' => 'zh-TW',
1544 'TJ' => 'tg-TJ',
1545 'TZ' => 'sw-TZ',
1546 'TH' => 'th-TH',
1547 'TL' => 'pt-TL',
1548 'TG' => 'fr-TG',
1549 'TO' => 'to-TO',
1550 'TT' => 'en-TT',
1551 'TN' => 'ar-TN',
1552 'TR' => 'tr-TR',
1553 'TM' => 'tk-TM',
1554 'TV' => 'en-TV',
1555 'UG' => 'en-UG',
1556 'UA' => 'uk-UA',
1557 'AE' => 'ar-AE',
1558 'GB' => 'en-GB',
1559 'US' => 'en-US',
1560 'UY' => 'es-UY',
1561 'UZ' => 'uz-UZ',
1562 'VU' => 'bi-VU',
1563 'VE' => 'es-VE',
1564 'VN' => 'vi-VN',
1565 'YE' => 'ar-YE',
1566 'ZM' => 'en-ZM',
1567 'ZW' => 'en-ZW'
1568 ];
1569 }
1570
1571 /**
1572 * Returns a translatable string with a shortcode inserted in the correct format.
1573 *
1574 * @param string $shortcode The shortcode to be inserted (e.g., '[fluent_cart_receipt]').
1575 * @return string The formatted translatable string.
1576 */
1577 public static function getShortcodeInstructionString(string $shortcode, $pageName = ''): string
1578 {
1579 $copyToClipboard = __('Copy to clipboard', 'fluent-cart');
1580 return sprintf(
1581 /* translators: %s: Shortcode */
1582 '<p>' . _x('Use %1$s shortcode in your page.', 'Shortcode instruction message', 'fluent-cart') . '</p>',
1583 '<code class="copyable-content" title="' . $copyToClipboard . '">' . ($shortcode) . '</code>',
1584 //$pageName
1585 );
1586 }
1587
1588
1589 /**
1590 * Get the current user Model.
1591 * @return User|\FluentCart\Framework\Database\Orm\Builder|\FluentCart\Framework\Database\Orm\Builder[]|\FluentCart\Framework\Database\Orm\Collection|\FluentCart\Framework\Database\Orm\Model|null
1592 */
1593 public static function getCurrentUser($refresh = false)
1594 {
1595 static $user = false;
1596
1597 if (!$refresh && $user !== false) {
1598 return $user;
1599 }
1600
1601 $userId = get_current_user_id();
1602 if (!$userId) {
1603 $user = null;
1604 return $user;
1605 }
1606
1607 $user = User::query()->find($userId);
1608
1609 return $user;
1610
1611 }
1612
1613 public static function hasLicense($product): bool
1614 {
1615 if (empty($product)) {
1616 return false;
1617 }
1618
1619 $meta = Arr::get($product, 'licensesMeta.meta_value', []);
1620
1621 if (empty($meta)) {
1622 return false;
1623 }
1624
1625 $meta = is_string($meta) ? json_decode($meta, true) : $meta;
1626
1627 return Arr::get($meta, 'enabled') === 'yes';
1628
1629 }
1630
1631
1632 public static function generateDownloadFileLink($productDownload, $orderId = null, $validityInMinutes = 60, $isAdmin = false): string
1633 {
1634 $identifier = Arr::get($productDownload, 'download_identifier', '');
1635
1636 $validityInMinutes = apply_filters('fluent_cart/download_link_validity_in_minutes', $validityInMinutes, [
1637 'product_download' => $productDownload,
1638 'order_id' => $orderId,
1639 'is_admin' => $isAdmin,
1640 ]);
1641
1642 $signParams = [
1643 'download_identifier' => $identifier,
1644 'valid_till' => DateTime::now()
1645 ->addMinutes($validityInMinutes ?? 60)
1646 ->getTimestamp()
1647 ];
1648
1649 if ($orderId) {
1650 $orderId = Arr::wrap($orderId);
1651 $signParams['order_id'] = json_encode($orderId);
1652 }
1653
1654 $url = (new BaseUrl())->sign(site_url('/'), $signParams);
1655
1656 return URL::appendQueryParams($url, [
1657 'fluent-cart' => 'download-by-id',
1658 ]);
1659
1660 }
1661
1662
1663 public static function readableFileSize($bytes): string
1664 {
1665 // Converts bytes to a human-readable format (e.g., KB, MB, GB)
1666 // Example: 1024 -> "1 KB"
1667 // Example: 1048576 -> "1 MB"
1668
1669 if (!$bytes && $bytes !== 0) return '';
1670 $units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
1671 $i = floor(log($bytes, 2) / 10);
1672 $size = $bytes / pow(1024, $i);
1673 return round($size, 2) . ' ' . $units[$i];
1674 }
1675
1676
1677 public static function getSitePrefix()
1678 {
1679 $siteUrl = rtrim(home_url(), '/');
1680 // remove http:// or https:// from the URL
1681 $siteUrl = preg_replace('#^https?://#', '', $siteUrl);
1682 $sitePrefix = str_replace(['/', '.'], '_', $siteUrl);
1683
1684 return apply_filters('fluent_cart/site_prefix', $sitePrefix, []);
1685 }
1686
1687 public static function humanIntervalMaps($interval = '')
1688 {
1689 $intervals = [
1690 'daily' => __('day', 'fluent-cart'),
1691 'weekly' => __('week', 'fluent-cart'),
1692 'monthly' => __('month', 'fluent-cart'),
1693 'quarterly' => __('quarter', 'fluent-cart'),
1694 'half_yearly' => __('six month', 'fluent-cart'),
1695 'yearly' => __('year', 'fluent-cart'),
1696 ];
1697
1698 return Arr::get($intervals, $interval);
1699 }
1700
1701 /**
1702 * @return array Array of intervals with label and value
1703 */
1704 public static function getAvailableSubscriptionIntervalOptions(): array
1705 {
1706 $intervals = [
1707 [
1708 'label' => __('Yearly', 'fluent-cart'),
1709 'value' => 'yearly',
1710 'map_value' => 'year',
1711 ],
1712 [
1713 'label' => __('Half Yearly', 'fluent-cart'),
1714 'value' => 'half_yearly',
1715 'map_value' => 'half_year',
1716 ],
1717 [
1718 'label' => __('Quarterly', 'fluent-cart'),
1719 'value' => 'quarterly',
1720 'map_value' => 'quarter',
1721 ],
1722 [
1723 'label' => __('Monthly', 'fluent-cart'),
1724 'value' => 'monthly',
1725 'map_value' => 'month',
1726 ],
1727 [
1728 'label' => __('Weekly', 'fluent-cart'),
1729 'value' => 'weekly',
1730 'map_value' => 'week',
1731 ],
1732 [
1733 'label' => __('Daily', 'fluent-cart'),
1734 'value' => 'daily',
1735 'map_value' => 'day',
1736 ]
1737 ];
1738
1739 return apply_filters('fluent_cart/available_subscription_interval_options', $intervals);
1740 }
1741
1742 public static function translateIntervalToStandardFormat($repeatInterval)
1743 {
1744 if (empty($repeatInterval)) {
1745 return 'year';
1746 }
1747
1748 $intervalMaps = static::getAvailableSubscriptionIntervalMaps();
1749
1750 if (!isset($intervalMaps[$repeatInterval])) {
1751 return 'year';
1752 }
1753
1754 return $intervalMaps[$repeatInterval];
1755 }
1756
1757 public static function getAvailableSubscriptionIntervalMaps()
1758 {
1759 $intervalOptions = static::getAvailableSubscriptionIntervalOptions();
1760
1761 $intervalMaps = [];
1762 foreach ($intervalOptions as $option) {
1763 $intervalMaps[$option['value']] = $option['map_value'];
1764 }
1765
1766 return $intervalMaps;
1767
1768 }
1769
1770 public static function calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval)
1771 {
1772 $intervalInDays = static::subscriptionIntervalInDays($repeatInterval);
1773
1774 $maxTrialDaysAllowed = apply_filters('fluent_cart/max_trial_days_allowed', 365, [
1775 'existing_trial_days' => $trialDays,
1776 'repeat_interval' => $repeatInterval,
1777 'interval_in_days' => $intervalInDays,
1778 ]);
1779
1780 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
1781
1782 }
1783
1784 public static function subscriptionIntervalInDays($interval)
1785 {
1786 switch ($interval) {
1787 case 'daily':
1788 return 1;
1789 case 'weekly':
1790 return 7;
1791 case 'monthly':
1792 return 30;
1793 case 'quarterly':
1794 return 90;
1795 case 'half_yearly':
1796 return 182;
1797 case 'yearly':
1798 return 365;
1799 default:
1800 return apply_filters('fluent_cart/subscription_interval_in_days', 0, [
1801 'interval' => $interval,
1802 ]);
1803 }
1804 }
1805
1806 public static function parseTermIdsForFilter($filters): array
1807 {
1808 $taxonomies = Taxonomy::getTaxonomies();
1809 if (is_string($filters)) {
1810 $filters = json_decode($filters, true);
1811 }
1812 $formattedFilters = [];
1813
1814 foreach ($taxonomies as $key => $taxonomy) {
1815 $terms = Arr::get($filters, $key, []);
1816 if (!is_array($terms)) {
1817 $terms = [$terms];
1818 }
1819
1820 $terms = array_filter($terms, function ($term) {
1821 return !empty($term);
1822 });
1823
1824 if (!empty($terms)) {
1825 $terms = array_map(function ($term) {
1826 return sanitize_text_field((string)$term);
1827 }, $terms);
1828
1829
1830 $formattedFilters[$key] = $terms;
1831 }
1832
1833
1834 }
1835
1836 return $formattedFilters;
1837 }
1838
1839 public static function mergeTermIdsForFilter($array1 = [], $array2 = []): array
1840 {
1841 $result = [];
1842
1843 foreach ([$array1, $array2] as $array) {
1844 foreach ($array as $key => $values) {
1845 if (!isset($result[$key])) {
1846 $result[$key] = [];
1847 }
1848 $result[$key] = array_values(array_unique(array_merge($result[$key], $values)));
1849 }
1850 }
1851
1852 return $result;
1853 }
1854
1855 public static function loadBundleChild(array $variants, $select = ['id', 'variation_title']): array
1856 {
1857 $allChildVariants = Arr::pluck($variants, 'other_info.bundle_child_ids');
1858
1859 $allChildVariants = array_unique(Arr::flatten($allChildVariants));
1860 $allChildVariants = Arr::except(
1861 $allChildVariants,
1862 Arr::pluck($variants, 'id')
1863 );
1864 $allChildVariants = array_filter($allChildVariants);
1865 $childVariants = ProductVariation::query()
1866 ->whereIn('id', $allChildVariants)
1867 ->with('product:ID,post_title')
1868 ->select($select)
1869 ->get()
1870 ->toArray();
1871
1872 // Extract post_title from product and remove product object to keep data clean
1873 foreach ($childVariants as $key => $childVariant) {
1874 $postTitle = Arr::get($childVariant, 'product.post_title');
1875 if ($postTitle) {
1876 $childVariants[$key]['post_title'] = $postTitle;
1877 unset($childVariants[$key]['product']);
1878 }
1879 }
1880
1881 foreach ($variants as &$variant) {
1882 $childIds = Arr::get($variant, 'other_info.bundle_child_ids', []);
1883 $variant['bundle_child_ids'] = $childIds;
1884 if (count($childIds) < 1) {
1885 $variant['child_variants'] = [];
1886 continue;
1887 }
1888 foreach ($childVariants as $childVariant) {
1889 if (in_array($childVariant['id'], Arr::get($variant, 'other_info.bundle_child_ids', []))) {
1890 $variant['child_variants'][$childVariant['id']] = $childVariant;
1891 }
1892 }
1893 }
1894
1895 return $variants;
1896 }
1897
1898 /**
1899 * Translate digits in a number according to the configured numeric system.
1900 *
1901 * Converts the digits 0-9 in the given number to their equivalents
1902 * defined in the numeric system string from `dateTimeStrings()`.
1903 * This allows displaying numbers in other numeral systems, e.g., Bengali, Arabic, Hindi, etc.
1904 *
1905 * Only digits are translated; other characters such as decimal points, currency symbols, or text remain unchanged.
1906 *
1907 * @param int|float|string $number The number to translate.
1908 * @return string The number with digits translated according to the configured numeric system.
1909 */
1910 public static function translateNumber($number): string
1911 {
1912 $config = TransStrings::dateTimeStrings();
1913 $numericSystem = Arr::get($config, 'numericSystem', '0_1_2_3_4_5_6_7_8_9');
1914 $digits = explode('_', $numericSystem);
1915
1916 return strtr(
1917 (string)$number,
1918 array_combine(range(0, 9),
1919 $digits)
1920 );
1921 }
1922
1923 public static function isModalCheckoutEnabled(): bool
1924 {
1925 //$storeSettings = new StoreSettings();
1926 //$enableModalCheckout = $storeSettings->get('enable_modal_checkout', 'no');
1927 return apply_filters('fluent_cart/enable_modal_checkout', false);
1928 }
1929
1930 public static function isAdminUser(): bool
1931 {
1932 return current_user_can('manage_options');
1933 }
1934
1935 /**
1936 * Convert string/boolean to actual boolean value.
1937 * Handles shortcode string attributes like "true"/"false"
1938 *
1939 * @param mixed $value The value to convert to boolean
1940 * @param bool $default Default value if conversion fails
1941 * @return bool The boolean result
1942 */
1943 public static function toBool($value, bool $default = false): bool
1944 {
1945 if (is_bool($value)) {
1946 return $value;
1947 }
1948
1949 if (is_string($value)) {
1950 $value = strtolower(trim($value));
1951 if (in_array($value, ['true', '1', 'yes', 'on'], true)) {
1952 return true;
1953 }
1954 if (in_array($value, ['false', '0', 'no', 'off'], true)) {
1955 return false;
1956 }
1957 }
1958
1959 return $default;
1960 }
1961
1962 public static function formatTaxRatePercent(float $rate): string
1963 {
1964 $formatted = number_format($rate, 4, '.', '');
1965 if (strpos($formatted, '.') !== false) {
1966 $formatted = rtrim($formatted, '0');
1967 $formatted = rtrim($formatted, '.');
1968 }
1969 return $formatted;
1970 }
1971
1972 /**
1973 * Returns the tax label for order-level tax rows (tax_total, not per-item).
1974 * For mixed orders, indicates tax varies per item.
1975 *
1976 * @param \FluentCart\App\Models\Order $order
1977 * @return string
1978 */
1979 public static function getOrderTaxLabel($order) {
1980 if ((int) $order->tax_behavior === 3) {
1981 return esc_html__('(Varies)', 'fluent-cart');
1982 }
1983 return (int) $order->tax_behavior === 2
1984 ? esc_html__('(Included)', 'fluent-cart')
1985 : esc_html__('(Excluded)', 'fluent-cart');
1986 }
1987 }
1988