/ license_*)
* utm_content = the exact placement, e.g. feature_lock_advanced_inventory
* utm_term = plugin version that generated the link
* utm_id = promo id, blank normally (omit unless passed)
*
* @param string $content The utm_content placement.
* @param array $overrides Override any utm_* param (e.g. utm_campaign for cross-sell).
* @return string
*/
public static function getUpgradeUrl($content = 'upgrade_page', $overrides = []): string
{
$baseUrl = (string) apply_filters(
'fluent_cart/pro_upgrade_base_url',
'https://fluentcart.com/discount-deal/'
);
$params = wp_parse_args($overrides, [
'utm_source' => 'fluent-cart',
'utm_medium' => App::isProActive() ? 'pro_plugin' : 'free_plugin',
'utm_campaign' => 'upgrade_pro',
'utm_content' => $content,
'utm_term' => defined('FLUENTCART_VERSION') ? FLUENTCART_VERSION : '',
]);
// Drop any blank params (e.g. an unset utm_id) so they never hit the URL.
$params = array_filter($params, function ($value) {
return $value !== '' && $value !== null;
});
return add_query_arg($params, $baseUrl);
}
public static function getRestInfo()
{
$app = App::getInstance();
$ns = $app->config->get('app.rest_namespace');
$ver = $app->config->get('app.rest_version');
return [
'base_url' => self::getBaseRestUrl(),
'url' => self::getFullRestUrl($ns, $ver),
'nonce' => wp_create_nonce('wp_rest'),
'namespace' => $ns,
'version' => $ver,
];
}
/**
* Get base rest url by examining the permalink.
*
* @see https://wordpress.stackexchange.com/questions/273144/can-i-use-rest-api-on-plain-permalink-format
*
* @return string
*/
protected static function getBaseRestUrl()
{
if (get_option('permalink_structure')) {
return esc_url_raw(rest_url());
}
return esc_url_raw(
rtrim(get_site_url(), '/') . "/?rest_route=/"
);
}
/**
* Get the full rest url by examining the permalink
* (full means, including the namespace/version).
*
* @see https://wordpress.stackexchange.com/questions/273144/can-i-use-rest-api-on-plain-permalink-format
*
* @return string
*/
protected static function getFullRestUrl($ns, $ver)
{
if (get_option('permalink_structure')) {
return esc_url_raw(rest_url($ns . '/' . $ver));
}
return esc_url_raw(
rtrim(get_site_url(), '/') . "/?rest_route=/{$ns}/{$ver}"
);
}
public static function shopConfig($key = false)
{
/**
* todo - need to review for more improvement : AR
*/
$currencySettings = (fluentCart(CurrencySettings::class))->get();
$storeSettings = (new StoreSettings())->get([
'store_name', 'store_logo', 'weight_unit', 'dimension_unit'
]);
$storeSettings['shipping_packages'] = self::getShippingPackages();
$settings = array_merge($currencySettings, $storeSettings);
if (!$key) {
return $settings;
}
if (is_array($key)) {
return Arr::only($settings, $key);
} else {
return Arr::get($settings, $key);
}
}
/**
* Convert weight between units using grams as base.
*
* @param float $value
* @param string $fromUnit
* @param string $toUnit
* @return float
*/
public static function convertWeight($value, $fromUnit, $toUnit)
{
if ($fromUnit === $toUnit || !$value) {
return (float)$value;
}
$toGrams = [
'g' => 1,
'kg' => 1000,
'lbs' => 453.592,
'oz' => 28.3495,
];
$fromFactor = isset($toGrams[$fromUnit]) ? $toGrams[$fromUnit] : 1;
$toFactor = isset($toGrams[$toUnit]) ? $toGrams[$toUnit] : 1;
$grams = $value * $fromFactor;
return round($grams / $toFactor, 6);
}
/**
* Get all shipping packages from store settings.
*
* @return array
*/
public static function getShippingPackages()
{
static $packages = null;
if ($packages === null) {
$packages = fluent_cart_get_option('shipping_packages', []);
}
return $packages ?: [];
}
/**
* Get the default shipping package.
*
* @return array|null
*/
public static function getDefaultPackage()
{
$packages = self::getShippingPackages();
foreach ($packages as $package) {
if (!empty($package['is_default'])) {
return $package;
}
}
return null;
}
/**
* Find a shipping package by slug.
*
* @param string $slug
* @return array|null
*/
public static function getPackageBySlug($slug)
{
if (!$slug) {
return self::getDefaultPackage();
}
$packages = self::getShippingPackages();
foreach ($packages as $package) {
if (isset($package['slug']) && $package['slug'] === $slug) {
return $package;
}
}
return self::getDefaultPackage();
}
public static function invoiceSettings($key = false)
{
$settings = [
'invoice_prefix' => 'AS-',
];
if (!$key) {
return $settings;
}
return Arr::get($settings, $key);
}
public static function getOrderStatuses()
{
$statuses = [
'on-hold' => __('On Hold', 'fluent-cart'),
'processing' => __('Processing', 'fluent-cart'),
'completed' => __('Completed', 'fluent-cart'),
//'archived' => __('Archived', 'fluent-cart'),
'cancelled' => __('Cancelled', 'fluent-cart'),
];
return apply_filters('fluent_cart/order_statuses', $statuses, []);
}
public static function getEditableOrderStatuses()
{
$statuses = [
'on-hold' => __('On Hold', 'fluent-cart'),
'processing' => __('Processing', 'fluent-cart'),
'completed' => __('Completed', 'fluent-cart'),
// 'archived' => __('Archived', 'fluent-cart'),
'cancelled' => __('Cancelled', 'fluent-cart')
];
return apply_filters('fluent_cart/editable_order_statuses', $statuses, []);
}
public static function getEditableCustomerStatuses()
{
$statuses = [
'active' => __('Active', 'fluent-cart'),
'inactive' => __('Inactive', 'fluent-cart'),
];
return apply_filters('fluent_cart/editable_customer_statuses', $statuses, []);
}
public static function getShippingStatuses()
{
$statuses = [
'unshipped' => __('Unshipped', 'fluent-cart'),
'shipped' => __('Shipped', 'fluent-cart'),
'delivered' => __('Delivered', 'fluent-cart'),
'unshippable' => __('Unshippable', 'fluent-cart'),
];
return apply_filters('fluent_cart/shipping_statuses', $statuses, []);
}
public static function getEditableShippingStatuses()
{
$statuses = [
'unshipped' => __('Unshipped', 'fluent-cart'),
'shipped' => __('Shipped', 'fluent-cart'),
'delivered' => __('Delivered', 'fluent-cart'),
'unshippable' => __('Unshippable', 'fluent-cart'),
];
return apply_filters('fluent_cart/editable_shipping_statuses', $statuses, []);
}
public static function getOrderSuccessStatuses()
{
return [
'completed',
// 'archived',
'processing',
];
}
public static function getOrderFailedStatuses()
{
return [
'failed',
//'refunded',
'cancelled',
];
}
public static function getTransactionSuccessStatuses()
{
return [
'paid',
];
}
public static function getTransactionStatuses($withLabel = true)
{
$statuses = [
'pending' => __('Pending', 'fluent-cart'),
'paid' => __('Paid', 'fluent-cart'),
'require_capture' => __('Authorized (Require Capture)', 'fluent-cart'),
'failed' => __('Failed', 'fluent-cart'),
'refunded' => __('Refunded', 'fluent-cart'),
'active' => __('Active', 'fluent-cart'),
];
$statuses = apply_filters('fluent_cart/transaction_statuses', $statuses, []);
if ($withLabel) {
return $statuses;
}
return array_keys($statuses);
}
public static function getEditableTransactionStatuses($withLabel = true)
{
$statuses = [
'pending' => __('Pending', 'fluent-cart'),
'paid' => __('Paid', 'fluent-cart'),
'failed' => __('Failed', 'fluent-cart'),
'refunded' => __('Refunded', 'fluent-cart'),
];
$statuses = apply_filters('fluent_cart/editable_transaction_statuses', $statuses, []);
if ($withLabel) {
return $statuses;
}
return array_keys($statuses);
}
public static function productStatuses($withLabel = true): array
{
$statues = [
'publish' => __('Publish', 'fluent-cart'),
'draft' => __('Draft', 'fluent-cart'),
'future' => __('Scheduled', 'fluent-cart'),
'private' => __('Private', 'fluent-cart'),
'trash' => __('Trashed', 'fluent-cart'),
];
if ($withLabel) {
return $statues;
}
return array_keys($statues);
}
public static function productAdminAllStatuses()
{
$statuses = self::productStatuses();
unset($statuses['trash']);
return array_keys($statuses);
}
public static function getCartDriver()
{
return 'db';
}
/**
* Convert an amount to a formatted decimal string.
*
* @param float $amount The amount to convert. (required)
* @param bool $withCurrency Whether to include the currency symbol. (optional, default: true)
* @param string|null $currencyCode The currency code to use. (optional, default: null)
* @param bool $formatted Whether to format the amount. (optional, default: true)
* @param bool $showDecimals Whether to show decimal places. (optional, default: true)
* @param bool $thousand_separator Whether to include thousand separators. (optional, default: true)
*
* @return string The formatted amount.
*
* @dev Note: If you don't want the thousand separator to be applied,
* you need to set $formatted to true and $thousand_separator to false.
*
* @hook fluent_cart/hide_unnecessary_decimals - Filter to control whether unnecessary decimals (like .00) should be hidden.
* Usage: add_filter('fluent_cart/hide_unnecessary_decimals', '__return_true'); // This will show 10 instead of 10.00
*/
public static function toDecimal($amount, $withCurrency = true, $currencyCode = null, $formatted = true, $showDecimals = true, $thousand_separator = true, $withTranslatedDigit = true)
{
if (!is_numeric($amount)) {
return $amount;
}
// Set default decimal places to 2
$decimal = 2;
// Check if the shop is using a zero-decimal currency
if (self::shopConfig('is_zero_decimal')) {
$decimal = 0;
}
// Use provided or default currency code
if (!$currencyCode) {
$currencyCode = self::shopConfig('currency');
}
// Get currency sign
$sign = CurrenciesHelper::getCurrencySign($currencyCode);
$amount = floatVal($amount / 100);
// If $showDecimals is false, we override decimal places to 0
if (!$showDecimals) {
$decimal = 0;
}
// Format the amount based on the decimal configuration
if ($formatted) {
$decimal_separator = self::shopConfig('decimal_separator') === 'comma' ? ',' : '.';
$thousand_separator = $decimal_separator === ',' ? '.' : ',';
// Check if we should hide unnecessary decimal places (e.g., 10.00 -> 10)
$hideUnnecessaryDecimals = apply_filters('fluent_cart/hide_unnecessary_decimals', false, [
'amount' => $amount,
'decimal' => $decimal
]);
$amount = number_format(
$amount,
$decimal,
$decimal_separator,
$thousand_separator
);
if ($hideUnnecessaryDecimals && $decimal > 0) {
// Remove trailing zeros only from the decimal portion
$parts = explode($decimal_separator, $amount);
if (count($parts) === 2) {
$parts[1] = rtrim($parts[1], '0');
if ($parts[1] === '') {
$amount = $parts[0];
} else {
$amount = $parts[0] . $decimal_separator . $parts[1];
}
}
}
if ($withTranslatedDigit) {
$amount = self::translateNumber($amount);
}
}
// If $withCurrency is false, just return the formatted amount
if (!$withCurrency) {
return $amount;
}
// Get currency position and return formatted amount with currency
$position = self::shopConfig('currency_position');
switch ($position) {
case 'before':
return $sign . $amount;
case 'after':
return $amount . $sign;
case 'iso_before':
return $currencyCode . ' ' . $amount;
case 'iso_after':
return $amount . ' ' . $currencyCode;
case 'symbool_before_iso':
return $sign . $amount . ' ' . $currencyCode;
case 'symbool_after_iso':
return $currencyCode . ' ' . $amount . $sign;
case 'symbool_and_iso':
return $currencyCode . ' ' . $sign . $amount;
default:
return $sign . $amount;
}
}
public static function toCent($amount): int
{
if (!is_numeric($amount)) {
return 0;
}
$amount = floatval($amount) * 100; // Convert to float and multiply
$amount = (int)round($amount); // Round to nearest integer, then cast
return $amount;
}
/**
* Normalize a value that is ALREADY in cents to a whole-cent int.
*
* Unlike toCent(), this does not scale — use it when the incoming value is a
* cents amount that may have arrived as a float. Clients computing cents in
* JavaScript send artifacts like 1998.9999999999998 for 1999, and a bare
* (int) cast truncates those, silently undercharging by a cent. Non-numeric
* input (including the null the request pipeline injects for omitted keys)
* normalizes to 0.
*
* Magnitudes beyond float's exact-integer range (2^53) are REJECTED, not
* coerced: is_numeric() accepts exponential notation like 1e19, and casting
* that float to int wraps to -8446744073709551616 — a VALID signed BIGINT —
* so the corrupt value would persist silently where an un-normalized float
* used to fail loudly at MySQL. No real cents amount approaches this bound.
*/
public static function roundCent($amount): int
{
if (!is_numeric($amount)) {
return 0;
}
$amount = (float) $amount;
if (!is_finite($amount) || $amount > 9.0e15 || $amount < -9.0e15) {
throw new \InvalidArgumentException(
'Value is out of the representable cents range: ' . var_export($amount, true)
);
}
return (int) round($amount);
}
public static function toDecimalWithoutComma($amount)
{
if (!is_numeric($amount)) {
return 0;
}
// Convert to float and divide by 100
$result = floatval($amount) / 100;
// Ensure exactly two decimal places
return round($result, 2);
}
public static function getCustomerByUser($user)
{
if (is_numeric($user)) {
$user = get_user_by('ID', $user);
}
if (!$user) {
return null;
}
return Customer::query()->where('user_id', $user->ID)
->orWhere('email', $user->user_email)
->first();
}
/**
* @param array $order_data
*
* @return array return ['billing_address','shipping_address','others'];
*/
/**
*
* @return string
*/
public static function getProductImageBaseUri(): string
{
$uploads = wp_upload_dir();
return $uploads['baseurl'] . '/' . FLUENTCART_UPLOAD_DIR . '/product_image/';
}
public static function getProductImageBaseDir()
{
$uploads = wp_upload_dir();
return $uploads['basedir'] . '/' . FLUENTCART_UPLOAD_DIR . '/product_image/';
}
public static function getAvailableCurrencyList()
{
$currencies = [
'BDT' => [
"label" => __('Bangladeshi Taka', 'fluent-cart'),
"value" => 'BDT',
"symbol" => '৳',
],
'USD' => [
"label" => __('United State Dollar', 'fluent-cart'),
"value" => 'USD',
"symbol" => '$',
],
'GBP' => [
"label" => __('United Kingdom', 'fluent-cart'),
"value" => 'GBP',
"symbol" => '£',
],
];
return apply_filters('fluent_cart/available_currencies', $currencies, []);
}
public static function getSymbolForCurrency($currency = 'BDT')
{
$symbol = '৳';
$list = self::getAvailableCurrencyList();
return $list[$currency]['symbol'] ?? $symbol;
}
public function getConfirmationSettings()
{
return (new Confirmation())->get();
}
/**
*
* @param $remove
* @return string
*/
public static function getCheckoutPageLinkAfterRemovingGetParams($remove = [])
{
global $fct_store;
$link = Arr::get($fct_store, 'checkout_link');
$link .= '?';
if (!empty($remove)) {
$params = App::request()->all();
foreach ($params as $key => $val) {
if (!in_array($key, $remove)) {
$link .= $key . '=' . $val . '&';
}
}
}
return rtrim($link, '&');
}
/**
*
* @return bool
*/
public static function isSingleProductPage(): bool
{
return is_singular([FluentProducts::CPT_NAME]);
}
public static function isTrue($array, $key)
{
$value = $array[$key] ?? false;
if (is_bool($value)) {
return $value;
}
if ($value === 'false' || !$value) {
return false;
}
return true;
}
public static function is_valid_json($string): bool
{
if (!is_string($string)) {
return false;
}
$trimmed = trim($string);
// Basic check: must start with { or [ and end with } or ]
if (!preg_match('/^(\{.*\}|\[.*\])$/s', $trimmed)) {
return false;
}
json_decode($trimmed);
return json_last_error() === JSON_ERROR_NONE;
}
public static function getStockStatuses($withLabel = true)
{
$statues = [
'in-stock' => __('In Stock', 'fluent-cart'),
'out-of-stock' => __('Out Of Stock', 'fluent-cart'),
];
if ($withLabel) {
return $statues;
}
return array_keys($statues);
}
public static function getFulfilmentTypes($withLabel = true)
{
$statues = [
'physical' => __('Physical', 'fluent-cart'),
'digital' => __('Digital', 'fluent-cart'),
];
if ($withLabel) {
return $statues;
}
return array_keys($statues);
}
public static function getVariationTypes($withLabel = true)
{
// advanced_variations is advertised in free too so the variation-type
// dropdown can offer it (shown Pro-locked with a crown and an upgrade
// modal while Pro is inactive). Pro re-registers the same key via the
// filter below when active.
$types = [
'simple' => __('Simple', 'fluent-cart'),
'simple_variations' => __('Simple Variations', 'fluent-cart'),
'advanced_variations' => __('Advanced Variations', 'fluent-cart'),
];
$types = apply_filters('fluent_cart/variation_types', $types);
if ($withLabel) {
return $types;
}
return array_keys($types);
}
public static function isValueEncrypted($raw_value)
{
if (!$raw_value || !is_string($raw_value) || !extension_loaded('openssl')) {
return false;
}
// Check if input is valid base64
if (!preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $raw_value)) {
return false;
}
$decoded = base64_decode($raw_value, true);
if ($decoded === false) {
return false;
}
// Check if decoded string is long enough for IV
$method = 'aes-256-ctr';
$ivlen = openssl_cipher_iv_length($method);
if (strlen($decoded) < $ivlen) {
return false;
}
$iv = substr($decoded, 0, $ivlen);
$ciphertext = substr($decoded, $ivlen);
$key = (defined('FLUENT_CART_ENCRYPTION_KEY'))
? FLUENT_CART_ENCRYPTION_KEY
: ((defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY)
? LOGGED_IN_KEY
: 'this-is-a-fallback-key-but-not-secure');
$salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT)
? LOGGED_IN_SALT
: 'this-is-a-fallback-salt-but-not-secure';
$value = openssl_decrypt($ciphertext, $method, $key, 0, $iv);
if ($value === false) {
return false;
}
return substr($value, -strlen($salt)) === $salt;
}
public static function encryptKey($value)
{
if (!$value) {
return $value;
}
if (!extension_loaded('openssl')) {
return $value;
}
if (self::isValueEncrypted($value)) {
return $value;
}
$salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
if (defined('FLUENT_CART_ENCRYPTION_KEY')) {
$key = FLUENT_CART_ENCRYPTION_KEY;
} else {
$key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
}
$method = 'aes-256-ctr';
$ivlen = openssl_cipher_iv_length($method);
$iv = openssl_random_pseudo_bytes($ivlen);
$raw_value = openssl_encrypt($value . $salt, $method, $key, 0, $iv);
if (!$raw_value) {
return false;
}
return base64_encode($iv . $raw_value); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
}
public static function decryptKey($raw_value)
{
if (!$raw_value) {
return $raw_value;
}
if (!extension_loaded('openssl')) {
return $raw_value;
}
$decoded = base64_decode($raw_value, true); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
if ($decoded === false) {
return $raw_value;
}
$method = 'aes-256-ctr';
$ivlen = openssl_cipher_iv_length($method);
if (strlen($decoded) <= $ivlen) {
return $raw_value;
}
$iv = substr($decoded, 0, $ivlen);
$ciphertext = substr($decoded, $ivlen);
if (defined('FLUENT_CART_ENCRYPTION_KEY')) {
$key = FLUENT_CART_ENCRYPTION_KEY;
} else {
$key = (defined('LOGGED_IN_KEY') && '' !== LOGGED_IN_KEY) ? LOGGED_IN_KEY : 'this-is-a-fallback-key-but-not-secure';
}
$salt = (defined('LOGGED_IN_SALT') && '' !== LOGGED_IN_SALT) ? LOGGED_IN_SALT : 'this-is-a-fallback-salt-but-not-secure';
$decrypted = openssl_decrypt($ciphertext, $method, $key, 0, $iv);
if (!$decrypted || substr($decrypted, -strlen($salt)) !== $salt) {
return false;
}
return substr($decrypted, 0, -strlen($salt));
}
/**
* @return array
* For kses_post to allow some html tags
*/
public static function allowedHTMLForCheckout(): array
{
$allowedTags = [
'svg' => [
'class' => true,
'aria-hidden' => true,
'aria-labelledby' => true,
'role' => true,
'xmlns' => true,
'width' => true,
'height' => true,
'viewbox' => true,
'fill' => true
],
'g' => [
'fill' => true
],
'title' => ['title' => true],
'path' => [
'd' => true,
'fill' => true,
'stroke' => true,
'fill-rule' => true,
'clip-rule' => true,
],
];
foreach (['input', 'label', 'div', 'span', 'p', 'select', 'option', 'textarea', 'button', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'] as $tag) {
$allowedTags[$tag] = [
'selected',
'type' => [],
"selected='selected'" => [],
'name' => [],
'value' => [],
'autocomplete' => [],
'placeholder' => [],
'data-required' => [],
'data-type' => [],
'id' => [],
'class' => [],
'required' => [],
'disabled' => [],
'for' => [],
'style' => [],
'checked' => [],
'maxlength' => [],
'data-id' => [],
'data-country' => [],
'data-fluent-cart-checkout-page-form-address-modal-open-button' => [],
'data-fluent-cart-checkout-page-form-address-modal-wrapper' => [],
'data-fluent-cart-checkout-page-form-input-wrapper' => [],
'data-fluent_cart_checkout_error' => [],
'data-fluent-cart-checkout-page-form-address-modal-body' => [],
'data-fluent-cart-checkout-page-form-address-modal-address-selector-button' => [],
'data-fluent-cart-checkout-page-form-address-input' => [],
'data-fluent-cart-checkout-page-form-address-select-wrapper' => [],
'data-fluent-cart-checkout-page-form-address-modal-close-button' => [],
'data-fluent-cart-checkout-page-form-address-modal-address-selector-button-wrapper' => [],
'data-fluent-cart-checkout-page-form-address-show-add-new-modal-button' => [],
'data-fluent-cart-checkout-page-form-address-modal-apply-button' => [],
'data-fluent-cart-checkout-page-form-address-show-add-new-modal-form-wrapper' => [],
'data-fluent-cart-checkout-page-form-address-show-add-new-modal-submit-button' => [],
'data-fluent-cart-checkout-page-form-address-show-add-new-modal-cancel-button' => [],
'data-fluent-cart-address-type' => [],
'data-fluent-cart-checkout-page-form-address-info-wrapper' => [],
'data-fluent-cart-checkout-page-form-error' => [],
'data-fluent-cart-checkout-page-form-section' => [],
'data-fluent-cart-checkout-page-discount-container' => [],
'data-fluent-cart-checkout-page-final-amount-container' => [],
'data-fluent-cart-checkout-page-new-total-amount' => [],
'data-fluent-cart-checkout-page-final-amount' => [],
'data-fluent-cart-checkout-page-coupon-validate' => [],
'data-fluent-cart-checkout-coupon-items-toggle' => [],
'data-fluent-cart-checkout-coupon-items-wrapper' => []
];
}
return $allowedTags;
}
public static function getCouponStatuses()
{
$statuses = [
'active' => __('Active', 'fluent-cart'),
'expired' => __('Expired', 'fluent-cart'),
'disabled' => __('Disabled', 'fluent-cart'),
];
return apply_filters('fluent_cart/coupon_statuses', $statuses, []);
}
public static function getCouponSuccessStatuses()
{
return [
'active'
];
}
/**
* Compact subscription terms as text
*
* Expected $data keys:
* - trial_days (int)
* - interval ('daily'|'weekly'|'monthly'|'yearly')
* - interval_count (int, default 1)
* - times (int; 0 = open-ended, >0 = finite number of payments/cycles)
* - price (string; formatted with currency)
* - signup_fee (string; 0 if none)
* - compare_price (string; 0 if none)
*
* Output examples:
* - "30 days free then $100.00 per year"
* - "$100 per year + $10 one-time signup fee"
* - "$100/month for 4 months"
* - "30 days free then $100/month for 4 months"
* - "$99 per month"
*/
public static function getSubscriptionTermText(array $data, $asHtml = false): string
{
// Fixed interval options
$intervalOptions = static::getAvailableSubscriptionIntervalMaps();
// Normalize / defaults
$trialDays = $data['trial_days'] ?? 0;
$interval = (string)($data['interval'] ? $data['interval'] : 'monthly');
$unit = '';
if (isset($intervalOptions[$interval])) {
$unit = $intervalOptions[$interval];
} else if ($interval) {
$intervalOptions = static::getAvailableSubscriptionIntervalOptions();
foreach ($intervalOptions as $option) {
if ($option['value'] === $interval) {
$unit = strtolower($option['label']);
break;
}
}
if (!$unit) {
$unit = strtolower(str_replace(['_', '-'], ' ', $interval));
}
}
if (!$unit) {
$unit = 'year';
}
$count = max(1, (int)($data['interval_count'] ?? 1));
$times = (int)($data['times'] ?? 0);
$price = (string)($data['price'] ?? '');
$signupFee = $data['signup_fee'] ?? '';
$compare = $data['compare_price'] ?? '';
$signupFeeLabel = $data['signup_fee_label'] ?? __('signup fee', 'fluent-cart');
// helpers
$pluralUnit = static function (string $unit, int $n): string {
switch ($unit) {
case 'day':
return _n('day', 'days', $n, 'fluent-cart');
case 'week':
return _n('week', 'weeks', $n, 'fluent-cart');
case 'quarter':
return _n('quarter', 'quarters', $n, 'fluent-cart');
case 'half_year':
return _n('six month', 'six months', $n, 'fluent-cart');
case 'year':
return _n('year', 'years', $n, 'fluent-cart');
case 'month':
return _n('month', 'months', $n, 'fluent-cart');
default:
// For custom intervals, add 's' for plural
return ($n > 1) ? $unit . 's' : $unit;
}
};
// Build the “per …” phrase (e.g., "per month", "per 3 months")
$perPhrase = ($count === 1)
? sprintf(
/* translators: %s is the singular unit name (e.g., day, month, year) */
__('per %s', 'fluent-cart'),
$pluralUnit($unit, 1)
)
: sprintf(
/* translators: %1$d is the count number, %2$s is the plural unit name (e.g., days, months, years) */
__('per %1$d %2$s', 'fluent-cart'),
$count,
$pluralUnit($unit, $count)
);
// Main price phrase:
// - If monthly installments (times > 0 and unit == month and count == 1): "$X/month for N months"
// - Else if times > 0: "$X per {count unit(s)} for N {cycle(s)}"
// - Else: "$X per {count unit(s)}"
if ($times > 0) {
if ($unit === 'month' && $count === 1) {
$installmentTail = sprintf(
/* translators: %s: pluralized 'month(s)' */
__('for %1$d %2$s', 'fluent-cart'),
$times,
$pluralUnit('month', $times)
);
$main = sprintf(
/* translators: Compact monthly installment, e.g. "$100/month for 4 months" */
__('%1$s/%2$s %3$s', 'fluent-cart'),
$price,
__('month', 'fluent-cart'),
$installmentTail
);
} else {
// Generic installment wording: "for N cycle(s)"
$cyclesTail = sprintf(
/* translators: %1$d is the number of cycles, %2$s is "cycle" or "cycles" */
__('for %1$d %2$s', 'fluent-cart'),
$times,
_n('cycle', 'cycles', $times, 'fluent-cart')
);
$main = sprintf(
/* translators: e.g. "$100 per 3 months for 4 cycles" */
__('%1$s %2$s %3$s', 'fluent-cart'),
$price,
$perPhrase,
$cyclesTail
);
}
} else {
// Open-ended subscription
$main = sprintf(
/* translators: e.g. "$99 per month" */
__('%1$s %2$s', 'fluent-cart'),
$price,
$perPhrase
);
}
// Prefix trial if present: "N days free then …"
if ($trialDays > 0) {
$trialFrag = sprintf(
/* translators: "30 days free then" */
__('%1$d %2$s free then', 'fluent-cart'),
$trialDays,
$pluralUnit('day', $trialDays)
);
$main = $trialFrag . ' ' . $main;
}
// Append signup fee if any: " + $10 one-time signup fee"
if ($signupFee) {
$main .= ' ' . sprintf(
/* translators: e.g. "+ $10 one-time signup fee" */
__('+ %1$s one-time %2$s', 'fluent-cart'),
$signupFee,
$signupFeeLabel
);
}
// No trailing period (matches your examples)
return $main;
}
public static function getTranslatedIntervalUnit(string $unit): string
{
switch ($unit) {
case 'day':
return __('day', 'fluent-cart');
case 'week':
return __('week', 'fluent-cart');
case 'month':
return __('month', 'fluent-cart');
case 'quarter':
return __('quarter', 'fluent-cart');
case 'half_year':
return __('six month', 'fluent-cart');
case 'year':
return __('year', 'fluent-cart');
default:
return $unit;
}
}
public static function generateSubscriptionInfo($otherInfo, $itemPrice, $currencyCode = null): ?string
{
// Convert to array only if it's an object
if (is_object($otherInfo)) {
$otherInfo = json_decode(json_encode($otherInfo), true);
}
$price = self::toDecimal($itemPrice, true, $currencyCode);
$recurringDiscountAmount = Arr::get($otherInfo, 'recurring_discounts.amount', 0);
if ($recurringDiscountAmount) {
$newRecurringAmount = $itemPrice - $recurringDiscountAmount;
$price = "" . $price . " " . self::toDecimal($newRecurringAmount, true, $currencyCode);
}
$repeatInterval = Arr::get($otherInfo, 'repeat_interval', '');
$occurrence = (int)Arr::get($otherInfo, 'times', 0);
$intervalOptions = static::getAvailableSubscriptionIntervalMaps();
$intervalUnit = '';
if (isset($intervalOptions[$repeatInterval])) {
$intervalUnit = $intervalOptions[$repeatInterval];
} else if ($repeatInterval) {
$intervalOptions = static::getAvailableSubscriptionIntervalOptions();
foreach ($intervalOptions as $option) {
if ($option['value'] === $repeatInterval) {
$intervalUnit = strtolower($option['label']);
break;
}
}
if (!$intervalUnit) {
$intervalUnit = ucwords(str_replace(['_', '-'], ' ', $repeatInterval));
}
}
$intervalLabel = Helper::getTranslatedIntervalUnit($intervalUnit);
$interval = $intervalUnit
? sprintf(
/* translators: %s is the interval (e.g., day, week, month, quarter, half_year, year) */
__('per %s', 'fluent-cart'),
$intervalLabel
)
: '';
$time = $intervalUnit
? $intervalLabel
: '';
$paymentInfo = sprintf(
/* 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 */
__('%1$s %2$s, for %3$s %4$s', 'fluent-cart'),
$price,
$interval,
$occurrence,
$time
);
if (empty($occurrence)) {
$paymentInfo = sprintf(
/* translators: %1$s is the price, %2$s is the interval, %3$s is "until cancel" text */
__('%1$s %2$s %3$s', 'fluent-cart'),
$price,
$interval,
__('until cancel', 'fluent-cart')
);
}
return !empty($otherInfo) ? $paymentInfo : null;
}
/**
* Billing-cycle text for a subscription with a config-defined schedule
* (see SubscriptionHelper::getBillingSchedule()) — cadences the
* billing_interval enum cannot express, e.g. "$100 per 3 years on Aug 19".
* Subscriptions without a schedule keep generateSubscriptionInfo().
*/
public static function generateScheduleSubscriptionInfo(array $schedule, $otherInfo, $itemPrice, $currencyCode = null): ?string
{
if (is_object($otherInfo)) {
$otherInfo = json_decode(json_encode($otherInfo), true);
}
$count = max(1, (int) Arr::get($schedule, 'interval', 1));
switch (Arr::get($schedule, 'period')) {
case 'day':
$unitLabel = _n('day', 'days', $count, 'fluent-cart');
break;
case 'week':
$unitLabel = _n('week', 'weeks', $count, 'fluent-cart');
break;
case 'month':
$unitLabel = _n('month', 'months', $count, 'fluent-cart');
break;
case 'year':
$unitLabel = _n('year', 'years', $count, 'fluent-cart');
break;
default:
return self::generateSubscriptionInfo($otherInfo, $itemPrice, $currencyCode);
}
$price = self::toDecimal($itemPrice, true, $currencyCode);
$recurringDiscountAmount = Arr::get($otherInfo, 'recurring_discounts.amount', 0);
if ($recurringDiscountAmount) {
$newRecurringAmount = $itemPrice - $recurringDiscountAmount;
$price = "" . $price . " " . self::toDecimal($newRecurringAmount, true, $currencyCode);
}
$interval = $count === 1
? sprintf(
/* translators: %s is the interval unit (e.g., day, week, month, year) */
__('per %s', 'fluent-cart'),
$unitLabel
)
: sprintf(
/* translators: %1$d is the count number, %2$s is the plural unit name (e.g., days, months, years) */
__('per %1$d %2$s', 'fluent-cart'),
$count,
$unitLabel
);
if ($anchorText = self::getScheduleAnchorText($schedule['period'], Arr::get($schedule, 'anchor', []))) {
$interval .= ' ' . $anchorText;
}
$occurrence = (int) Arr::get($otherInfo, 'times', 0);
if (empty($occurrence)) {
return sprintf(
/* translators: %1$s is the price, %2$s is the interval, %3$s is "until cancel" text */
__('%1$s %2$s %3$s', 'fluent-cart'),
$price,
$interval,
__('until cancel', 'fluent-cart')
);
}
return sprintf(
/* translators: %1$s is the price, %2$s is the interval, %3$s is the occurrence count, %4$s is "cycle(s)" */
__('%1$s %2$s, for %3$s %4$s', 'fluent-cart'),
$price,
$interval,
$occurrence,
_n('cycle', 'cycles', $occurrence, 'fluent-cart')
);
}
/**
* Human-readable billing anchor, e.g. "on Friday", "on the 10th",
* "on the last day", "on Aug 19". Anchor day 31 encodes "last day of
* the month" (see SubscriptionHelper::getBillingSchedule()).
*/
private static function getScheduleAnchorText(string $period, $anchor): string
{
if (!is_array($anchor) || !$anchor) {
return '';
}
if ($period === 'week' && !empty($anchor['weekday'])) {
$weekdays = [
1 => __('Monday', 'fluent-cart'),
2 => __('Tuesday', 'fluent-cart'),
3 => __('Wednesday', 'fluent-cart'),
4 => __('Thursday', 'fluent-cart'),
5 => __('Friday', 'fluent-cart'),
6 => __('Saturday', 'fluent-cart'),
7 => __('Sunday', 'fluent-cart'),
];
if (isset($weekdays[$anchor['weekday']])) {
/* translators: %s is a weekday name, e.g. "on Friday" */
return sprintf(__('on %s', 'fluent-cart'), $weekdays[$anchor['weekday']]);
}
return '';
}
if ($period === 'month' && !empty($anchor['day'])) {
$day = (int) $anchor['day'];
if ($day === 31) {
return __('on the last day', 'fluent-cart');
}
/* translators: %s is an ordinal day of month, e.g. "on the 10th" */
return sprintf(__('on the %s', 'fluent-cart'), gmdate('jS', gmmktime(12, 0, 0, 1, $day, 2001)));
}
if ($period === 'year' && (!empty($anchor['day']) || !empty($anchor['month']))) {
$day = (int) Arr::get($anchor, 'day', 0);
$month = (int) Arr::get($anchor, 'month', 0);
if ($month && $day) {
/* translators: %s is a date, e.g. "on Aug 19" */
return sprintf(__('on %s', 'fluent-cart'), gmdate('M', gmmktime(12, 0, 0, $month, 1, 2001)) . ' ' . $day);
}
if ($month) {
/* translators: %s is a month name, e.g. "in August" */
return sprintf(__('in %s', 'fluent-cart'), gmdate('F', gmmktime(12, 0, 0, $month, 1, 2001)));
}
/* translators: %s is an ordinal day of month, e.g. "on the 10th" */
return sprintf(__('on the %s', 'fluent-cart'), gmdate('jS', gmmktime(12, 0, 0, 1, $day, 2001)));
}
return '';
}
public static function generateSetupFeeInfo($otherInfo, $asArray = false)
{
// Convert to array if it's an object
if (is_object($otherInfo)) {
$otherInfo = json_decode(json_encode($otherInfo), true);
}
$signupFeeName = Arr::get($otherInfo, 'signup_fee_name', __('Setup Fee', 'fluent-cart'));
$fee = Arr::get($otherInfo, 'signup_fee', 0);
$manageSetupFee = Arr::get($otherInfo, 'manage_setup_fee', 'no');
if ($manageSetupFee !== 'yes' || !$fee) {
return '';
}
if ($originalSetupFee = Arr::get($otherInfo, 'original_signup_fee', 0)) {
if ($fee != $originalSetupFee) {
$title = __('Adjusted setup fee', 'fluent-cart');
$formattedAmount = CurrencySettings::getPriceHtml($fee, null, true, true);
if ($asArray) {
return [
'signup_fee_name' => $title,
'signup_fee' => $fee,
'signup_fee_formatted' => $formattedAmount,
];
}
return $title . $formattedAmount;
}
}
$formattedAmount = CurrencySettings::getPriceHtml($fee, null, true, true);
if ($asArray) {
return [
'signup_fee_name' => $signupFeeName,
'signup_fee' => $fee,
'signup_fee_formatted' => $formattedAmount,
];
}
return $signupFeeName . ' ' . $formattedAmount;
}
public static function generateTrialInfo($otherInfo)
{
$trialInfo = '';
$trialDays = Arr::get($otherInfo, 'trial_days', 0);
if ($trialDays && Arr::get($otherInfo, 'is_trial_days_simulated', 'no') !== 'yes') {
$trialInfo = sprintf(
/* translators: %d is the number of trial days */
__('Free Trial: %d days', 'fluent-cart'),
$trialDays
);
}
return apply_filters('fluent_cart/trial_info', $trialInfo, $otherInfo);
}
public static function getCountryList(): array
{
$options = App::getInstance('localization')->countriesOptions();
return apply_filters('fluent_cart/util/countries', $options, []);
}
public static function getCountyIsoLists(): array
{
return App::getInstance('localization')->getCountyIsoLists();
}
public static function getCountryCode($country_name)
{
$countries = self::getCountryList();
foreach ($countries as $country) {
if ($country['name'] === $country_name) {
return $country['value'];
}
}
return '';
}
/**
* Get the country's name with country code,
*
* @param $code
* @return string
*/
public static function getCountryName($code): string
{
if (!$code || !is_string($code)) {
return '';
}
$countries = self::getCountyIsoLists();
return $countries[$code] ?? $code;
}
public static function languageCodes(): array
{
return $langCodes = [
'AF' => 'fa-AF',
'AL' => 'sq-AL',
'DZ' => 'ar-DZ',
'AS' => 'sm-AS',
'AD' => 'ca-AD',
'AO' => 'pt-AO',
'AR' => 'es-AR',
'AM' => 'hy-AM',
'AU' => 'en-AU',
'AT' => 'de-AT',
'AZ' => 'az-AZ',
'BH' => 'ar-BH',
'BD' => 'bn-BD',
'BY' => 'be-BY',
'BE' => 'nl-BE',
'BZ' => 'en-BZ',
'BJ' => 'fr-BJ',
'BT' => 'dz-BT',
'BO' => 'es-BO',
'BA' => 'bs-BA',
'BW' => 'en-BW',
'BR' => 'pt-BR',
'BN' => 'ms-BN',
'BG' => 'bg-BG',
'BF' => 'fr-BF',
'BI' => 'fr-BI',
'KH' => 'km-KH',
'CM' => 'en-CM',
'CA' => 'en-CA',
'CV' => 'pt-CV',
'CF' => 'fr-CF',
'TD' => 'fr-TD',
'CL' => 'es-CL',
'CN' => 'zh-CN',
'CO' => 'es-CO',
'KM' => 'ar-KM',
'CD' => 'fr-CD',
'CG' => 'fr-CG',
'CR' => 'es-CR',
'CI' => 'fr-CI',
'HR' => 'hr-HR',
'CU' => 'es-CU',
'CY' => 'el-CY',
'CZ' => 'cs-CZ',
'DK' => 'da-DK',
'DJ' => 'fr-DJ',
'DM' => 'en-DM',
'DO' => 'es-DO',
'EC' => 'es-EC',
'EG' => 'ar-EG',
'SV' => 'es-SV',
'GQ' => 'es-GQ',
'ER' => 'ti-ER',
'EE' => 'et-EE',
'ET' => 'am-ET',
'FJ' => 'en-FJ',
'FI' => 'fi-FI',
'FR' => 'fr-FR',
'GA' => 'fr-GA',
'GM' => 'en-GM',
'GE' => 'ka-GE',
'DE' => 'de-DE',
'GH' => 'en-GH',
'GR' => 'el-GR',
'GD' => 'en-GD',
'GT' => 'es-GT',
'GN' => 'fr-GN',
'GW' => 'pt-GW',
'GY' => 'en-GY',
'HT' => 'fr-HT',
'HN' => 'es-HN',
'HU' => 'hu-HU',
'IS' => 'is-IS',
'IN' => 'hi-IN',
'ID' => 'id-ID',
'IR' => 'fa-IR',
'IQ' => 'ar-IQ',
'IE' => 'en-IE',
'IL' => 'he-IL',
'IT' => 'it-IT',
'JM' => 'en-JM',
'JP' => 'ja-JP',
'JO' => 'ar-JO',
'KZ' => 'kk-KZ',
'KE' => 'sw-KE',
'KI' => 'en-KI',
'KR' => 'ko-KR',
'KW' => 'ar-KW',
'KG' => 'ky-KG',
'LA' => 'lo-LA',
'LV' => 'lv-LV',
'LB' => 'ar-LB',
'LS' => 'en-LS',
'LR' => 'en-LR',
'LY' => 'ar-LY',
'LI' => 'de-LI',
'LT' => 'lt-LT',
'LU' => 'lb-LU',
'MG' => 'mg-MG',
'MW' => 'en-MW',
'MY' => 'ms-MY',
'MV' => 'dv-MV',
'ML' => 'fr-ML',
'MT' => 'mt-MT',
'MH' => 'mh-MH',
'MR' => 'ar-MR',
'MU' => 'mfe-MU',
'MX' => 'es-MX',
'FM' => 'en-FM',
'MD' => 'ro-MD',
'MC' => 'fr-MC',
'MN' => 'mn-MN',
'ME' => 'sr-ME',
'MA' => 'ar-MA',
'MZ' => 'pt-MZ',
'NA' => 'en-NA',
'NR' => 'en-NR',
'NP' => 'ne-NP',
'NL' => 'nl-NL',
'NZ' => 'en-NZ',
'NI' => 'es-NI',
'NG' => 'en-NG',
'NO' => 'no-NO',
'OM' => 'ar-OM',
'PK' => 'ur-PK',
'PW' => 'en-PW',
'PA' => 'es-PA',
'PG' => 'en-PG',
'PY' => 'es-PY',
'PE' => 'es-PE',
'PH' => 'en-PH',
'PL' => 'pl-PL',
'PT' => 'pt-PT',
'QA' => 'ar-QA',
'RO' => 'ro-RO',
'RU' => 'ru-RU',
'RW' => 'rw-RW',
'WS' => 'sm-WS',
'SM' => 'it-SM',
'SA' => 'ar-SA',
'SN' => 'fr-SN',
'RS' => 'sr-RS',
'SC' => 'fr-SC',
'SL' => 'en-SL',
'SG' => 'en-SG',
'SK' => 'sk-SK',
'SI' => 'sl-SI',
'SB' => 'en-SB',
'SO' => 'so-SO',
'ZA' => 'en-ZA',
'ES' => 'es-ES',
'LK' => 'si-LK',
'SD' => 'ar-SD',
'SR' => 'nl-SR',
'SZ' => 'en-SZ',
'SE' => 'sv-SE',
'CH' => 'de-CH',
'SY' => 'ar-SY',
'TW' => 'zh-TW',
'TJ' => 'tg-TJ',
'TZ' => 'sw-TZ',
'TH' => 'th-TH',
'TL' => 'pt-TL',
'TG' => 'fr-TG',
'TO' => 'to-TO',
'TT' => 'en-TT',
'TN' => 'ar-TN',
'TR' => 'tr-TR',
'TM' => 'tk-TM',
'TV' => 'en-TV',
'UG' => 'en-UG',
'UA' => 'uk-UA',
'AE' => 'ar-AE',
'GB' => 'en-GB',
'US' => 'en-US',
'UY' => 'es-UY',
'UZ' => 'uz-UZ',
'VU' => 'bi-VU',
'VE' => 'es-VE',
'VN' => 'vi-VN',
'YE' => 'ar-YE',
'ZM' => 'en-ZM',
'ZW' => 'en-ZW'
];
}
/**
* Returns a translatable string with a shortcode inserted in the correct format.
*
* @param string $shortcode The shortcode to be inserted (e.g., '[fluent_cart_receipt]').
* @return string The formatted translatable string.
*/
public static function getShortcodeInstructionString(string $shortcode, $pageName = ''): string
{
$copyToClipboard = __('Copy to clipboard', 'fluent-cart');
return sprintf(
/* translators: %s: Shortcode */
'
' . _x('Use %1$s shortcode in your page.', 'Shortcode instruction message', 'fluent-cart') . '
', '' . ($shortcode) . '',
//$pageName
);
}
/**
* Get the current user Model.
* @return User|\FluentCart\Framework\Database\Orm\Builder|\FluentCart\Framework\Database\Orm\Builder[]|\FluentCart\Framework\Database\Orm\Collection|\FluentCart\Framework\Database\Orm\Model|null
*/
public static function getCurrentUser($refresh = false)
{
static $user = false;
if (!$refresh && $user !== false) {
return $user;
}
$userId = get_current_user_id();
if (!$userId) {
$user = null;
return $user;
}
$user = User::query()->find($userId);
return $user;
}
public static function hasLicense($product): bool
{
if (empty($product)) {
return false;
}
$meta = Arr::get($product, 'licensesMeta.meta_value', []);
if (empty($meta)) {
return false;
}
$meta = is_string($meta) ? json_decode($meta, true) : $meta;
return Arr::get($meta, 'enabled') === 'yes';
}
public static function generateDownloadFileLink($productDownload, $orderId = null, $validityInMinutes = 60, $isAdmin = false): string
{
$identifier = Arr::get($productDownload, 'download_identifier', '');
$validityInMinutes = apply_filters('fluent_cart/download_link_validity_in_minutes', $validityInMinutes, [
'product_download' => $productDownload,
'order_id' => $orderId,
'is_admin' => $isAdmin,
]);
$signParams = [
'download_identifier' => $identifier,
'valid_till' => DateTime::now()
->addMinutes($validityInMinutes ?? 60)
->getTimestamp()
];
if ($orderId) {
$orderId = Arr::wrap($orderId);
$signParams['order_id'] = json_encode($orderId);
}
$url = (new BaseUrl())->sign(site_url('/'), $signParams);
return URL::appendQueryParams($url, [
'fluent-cart' => 'download-by-id',
]);
}
public static function readableFileSize($bytes): string
{
// Converts bytes to a human-readable format (e.g., KB, MB, GB)
// Example: 1024 -> "1 KB"
// Example: 1048576 -> "1 MB"
if (!$bytes && $bytes !== 0) return '';
$units = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
$i = floor(log($bytes, 2) / 10);
$size = $bytes / pow(1024, $i);
return round($size, 2) . ' ' . $units[$i];
}
public static function getSitePrefix()
{
$siteUrl = rtrim(home_url(), '/');
// remove http:// or https:// from the URL
$siteUrl = preg_replace('#^https?://#', '', $siteUrl);
$sitePrefix = str_replace(['/', '.'], '_', $siteUrl);
return apply_filters('fluent_cart/site_prefix', $sitePrefix, []);
}
public static function humanIntervalMaps($interval = '')
{
$intervals = [
'daily' => __('day', 'fluent-cart'),
'weekly' => __('week', 'fluent-cart'),
'monthly' => __('month', 'fluent-cart'),
'quarterly' => __('quarter', 'fluent-cart'),
'half_yearly' => __('six month', 'fluent-cart'),
'yearly' => __('year', 'fluent-cart'),
];
return Arr::get($intervals, $interval);
}
/**
* @return array Array of intervals with label and value
*/
public static function getAvailableSubscriptionIntervalOptions(): array
{
$intervals = [
[
'label' => __('Yearly', 'fluent-cart'),
'value' => 'yearly',
'map_value' => 'year',
],
[
'label' => __('Half Yearly', 'fluent-cart'),
'value' => 'half_yearly',
'map_value' => 'half_year',
],
[
'label' => __('Quarterly', 'fluent-cart'),
'value' => 'quarterly',
'map_value' => 'quarter',
],
[
'label' => __('Monthly', 'fluent-cart'),
'value' => 'monthly',
'map_value' => 'month',
],
[
'label' => __('Weekly', 'fluent-cart'),
'value' => 'weekly',
'map_value' => 'week',
],
[
'label' => __('Daily', 'fluent-cart'),
'value' => 'daily',
'map_value' => 'day',
]
];
return apply_filters('fluent_cart/available_subscription_interval_options', $intervals);
}
public static function translateIntervalToStandardFormat($repeatInterval)
{
if (empty($repeatInterval)) {
return 'year';
}
$intervalMaps = static::getAvailableSubscriptionIntervalMaps();
if (!isset($intervalMaps[$repeatInterval])) {
return 'year';
}
return $intervalMaps[$repeatInterval];
}
public static function getAvailableSubscriptionIntervalMaps()
{
$intervalOptions = static::getAvailableSubscriptionIntervalOptions();
$intervalMaps = [];
foreach ($intervalOptions as $option) {
$intervalMaps[$option['value']] = $option['map_value'];
}
return $intervalMaps;
}
public static function calculateAdjustedTrialDaysForInterval($trialDays, $repeatInterval)
{
$intervalInDays = static::subscriptionIntervalInDays($repeatInterval);
$maxTrialDaysAllowed = apply_filters('fluent_cart/max_trial_days_allowed', 365, [
'existing_trial_days' => $trialDays,
'repeat_interval' => $repeatInterval,
'interval_in_days' => $intervalInDays,
]);
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
}
public static function subscriptionIntervalInDays($interval)
{
return PaymentHelper::getIntervalDays($interval);
}
public static function parseTermIdsForFilter($filters): array
{
$taxonomies = Taxonomy::getTaxonomies();
if (is_string($filters)) {
$filters = json_decode($filters, true);
}
$formattedFilters = [];
foreach ($taxonomies as $key => $taxonomy) {
$terms = Arr::get($filters, $key, []);
if (!is_array($terms)) {
$terms = [$terms];
}
$terms = array_filter($terms, function ($term) {
return !empty($term);
});
if (!empty($terms)) {
$terms = array_map(function ($term) {
return sanitize_text_field((string)$term);
}, $terms);
$formattedFilters[$key] = $terms;
}
}
return $formattedFilters;
}
public static function mergeTermIdsForFilter($array1 = [], $array2 = []): array
{
$result = [];
foreach ([$array1, $array2] as $array) {
foreach ($array as $key => $values) {
if (!isset($result[$key])) {
$result[$key] = [];
}
$result[$key] = array_values(array_unique(array_merge($result[$key], $values)));
}
}
return $result;
}
public static function loadBundleChild(array $variants, $select = ['id', 'variation_title']): array
{
$allChildVariants = Arr::pluck($variants, 'other_info.bundle_child_ids');
$allChildVariants = array_unique(Arr::flatten($allChildVariants));
$allChildVariants = array_values(array_diff(
$allChildVariants,
Arr::pluck($variants, 'id')
));
$allChildVariants = array_filter($allChildVariants);
$childVariants = ProductVariation::query()
->whereIn('id', $allChildVariants)
->with('product:ID,post_title')
->select($select)
->get()
->toArray();
// Extract post_title from product and remove product object to keep data clean
foreach ($childVariants as $key => $childVariant) {
$postTitle = Arr::get($childVariant, 'product.post_title');
if ($postTitle) {
$childVariants[$key]['post_title'] = $postTitle;
unset($childVariants[$key]['product']);
}
}
foreach ($variants as &$variant) {
$childIds = Arr::get($variant, 'other_info.bundle_child_ids', []);
$variant['bundle_child_ids'] = $childIds;
if (count($childIds) < 1) {
$variant['child_variants'] = [];
continue;
}
foreach ($childVariants as $childVariant) {
if (in_array($childVariant['id'], Arr::get($variant, 'other_info.bundle_child_ids', []))) {
$variant['child_variants'][$childVariant['id']] = $childVariant;
}
}
}
return $variants;
}
/**
* Translate digits in a number according to the configured numeric system.
*
* Converts the digits 0-9 in the given number to their equivalents
* defined in the numeric system string from `dateTimeStrings()`.
* This allows displaying numbers in other numeral systems, e.g., Bengali, Arabic, Hindi, etc.
*
* Only digits are translated; other characters such as decimal points, currency symbols, or text remain unchanged.
*
* @param int|float|string $number The number to translate.
* @return string The number with digits translated according to the configured numeric system.
*/
public static function translateNumber($number): string
{
$config = TransStrings::dateTimeStrings();
$numericSystem = Arr::get($config, 'numericSystem', '0_1_2_3_4_5_6_7_8_9');
$digits = explode('_', $numericSystem);
return strtr(
(string)$number,
array_combine(range(0, 9),
$digits)
);
}
public static function isModalCheckoutEnabled(): bool
{
//$storeSettings = new StoreSettings();
//$enableModalCheckout = $storeSettings->get('enable_modal_checkout', 'no');
return apply_filters('fluent_cart/enable_modal_checkout', false);
}
public static function isAdminUser(): bool
{
return current_user_can('manage_options');
}
/**
* Convert string/boolean to actual boolean value.
* Handles shortcode string attributes like "true"/"false"
*
* @param mixed $value The value to convert to boolean
* @param bool $default Default value if conversion fails
* @return bool The boolean result
*/
public static function toBool($value, bool $default = false): bool
{
if (is_bool($value)) {
return $value;
}
if (is_string($value)) {
$value = strtolower(trim($value));
if (in_array($value, ['true', '1', 'yes', 'on'], true)) {
return true;
}
if (in_array($value, ['false', '0', 'no', 'off'], true)) {
return false;
}
}
return $default;
}
public static function formatTaxRatePercent(float $rate): string
{
$formatted = number_format($rate, 4, '.', '');
if (strpos($formatted, '.') !== false) {
$formatted = rtrim($formatted, '0');
$formatted = rtrim($formatted, '.');
}
return $formatted;
}
/**
* Returns the tax label for order-level tax rows (tax_total, not per-item).
* For mixed orders, indicates tax varies per item.
*
* @param \FluentCart\App\Models\Order $order
* @return string
*/
public static function getOrderTaxLabel($order) {
if ((int) $order->tax_behavior === 3) {
return esc_html__('(Varies)', 'fluent-cart');
}
return (int) $order->tax_behavior === 2
? esc_html__('(Included)', 'fluent-cart')
: esc_html__('(Excluded)', 'fluent-cart');
}
}