| @@ -11,8 +11,9 @@ | ||
| 11 | 11 | use FluentCart\App\Models\Customer; |
| 12 | 12 | use FluentCart\App\Models\ProductVariation; |
| 13 | 13 | use FluentCart\App\Models\User; |
| 14 | 14 | use FluentCart\App\Services\DateTime\DateTime; |
| 15 | +use FluentCart\App\Services\Payments\PaymentHelper; | |
| 15 | 16 | use FluentCart\App\Services\Localization\LocalizationManager; |
| 16 | 17 | use FluentCart\App\Services\Translations\TransStrings; |
| 17 | 18 | use FluentCart\App\Services\URL; |
| 18 | 19 | use FluentCart\Framework\Http\URL as BaseUrl; |
| @@ -32,8 +33,33 @@ | ||
| 32 | 33 | const ROLE_CAPABILITY_PREFIX = 'fluent_cart/permissions/'; |
| 33 | 34 | |
| 34 | 35 | const USER_ROLE = 'fluent_cart_customer'; |
| 35 | 36 | |
| 37 | + const MIN_INSTALLMENT_TIMES = 2; | |
| 38 | + | |
| 39 | + /** | |
| 40 | + * An installment plan must bill at least twice. times = 0 means unlimited | |
| 41 | + * (a plain recurring subscription) and times = 1 collects a single payment, | |
| 42 | + * which is a one-time purchase, not an installment plan. | |
| 43 | + * | |
| 44 | + * @param array $otherInfo A variant's other_info payload | |
| 45 | + * @return string|null Error message, or null when valid | |
| 46 | + */ | |
| 47 | + public static function installmentTimesError($otherInfo) | |
| 48 | + { | |
| 49 | + if (Arr::get($otherInfo, 'installment', 'no') !== 'yes') { | |
| 50 | + return null; | |
| 51 | + } | |
| 52 | + | |
| 53 | + $times = Arr::get($otherInfo, 'times', 0); | |
| 54 | + | |
| 55 | + if (!is_numeric($times) || (int)$times < static::MIN_INSTALLMENT_TIMES) { | |
| 56 | + 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'); | |
| 57 | + } | |
| 58 | + | |
| 59 | + return null; | |
| 60 | + } | |
| 61 | + | |
| 36 | 62 | public static function getUidSerial() |
| 37 | 63 | { |
| 38 | 64 | static $id = 0; |
| 39 | 65 | |
| @@ -42,8 +68,46 @@ | ||
| 42 | 68 | return $id; |
| 43 | 69 | |
| 44 | 70 | } |
| 45 | 71 | |
| 72 | + /** | |
| 73 | + * Build a spec-compliant "Upgrade to Pro" URL. | |
| 74 | + * | |
| 75 | + * Follows the shared Fluent* UTM spec: | |
| 76 | + * utm_source = fluent-cart (fixed vocabulary, never the wp.org slug) | |
| 77 | + * utm_medium = free_plugin | pro_plugin (acquisition vs cross-sell) | |
| 78 | + * utm_campaign= upgrade_pro (override for xsell_<target> / license_*) | |
| 79 | + * utm_content = the exact placement, e.g. feature_lock_advanced_inventory | |
| 80 | + * utm_term = plugin version that generated the link | |
| 81 | + * utm_id = promo id, blank normally (omit unless passed) | |
| 82 | + * | |
| 83 | + * @param string $content The utm_content placement. | |
| 84 | + * @param array $overrides Override any utm_* param (e.g. utm_campaign for cross-sell). | |
| 85 | + * @return string | |
| 86 | + */ | |
| 87 | + public static function getUpgradeUrl($content = 'upgrade_page', $overrides = []): string | |
| 88 | + { | |
| 89 | + $baseUrl = (string) apply_filters( | |
| 90 | + 'fluent_cart/pro_upgrade_base_url', | |
| 91 | + 'https://fluentcart.com/discount-deal/' | |
| 92 | + ); | |
| 93 | + | |
| 94 | + $params = wp_parse_args($overrides, [ | |
| 95 | + 'utm_source' => 'fluent-cart', | |
| 96 | + 'utm_medium' => App::isProActive() ? 'pro_plugin' : 'free_plugin', | |
| 97 | + 'utm_campaign' => 'upgrade_pro', | |
| 98 | + 'utm_content' => $content, | |
| 99 | + 'utm_term' => defined('FLUENTCART_VERSION') ? FLUENTCART_VERSION : '', | |
| 100 | + ]); | |
| 101 | + | |
| 102 | + // Drop any blank params (e.g. an unset utm_id) so they never hit the URL. | |
| 103 | + $params = array_filter($params, function ($value) { | |
| 104 | + return $value !== '' && $value !== null; | |
| 105 | + }); | |
| 106 | + | |
| 107 | + return add_query_arg($params, $baseUrl); | |
| 108 | + } | |
| 109 | + | |
| 46 | 110 | public static function getRestInfo() |
| 47 | 111 | { |
| 48 | 112 | $app = App::getInstance(); |
| 49 | 113 | |
| @@ -134,9 +198,9 @@ | ||
| 134 | 198 | */ |
| 135 | 199 | public static function convertWeight($value, $fromUnit, $toUnit) |
| 136 | 200 | { |
| 137 | 201 | if ($fromUnit === $toUnit || !$value) { |
| 138 | - return (float) $value; | |
| 202 | + return (float)$value; | |
| 139 | 203 | } |
| 140 | 204 | |
| 141 | 205 | $toGrams = [ |
| 142 | 206 | 'g' => 1, |
| @@ -178,9 +242,9 @@ | ||
| 178 | 242 | if (!empty($package['is_default'])) { |
| 179 | 243 | return $package; |
| 180 | 244 | } |
| 181 | 245 | } |
| 182 | - return !empty($packages) ? $packages[0] : null; | |
| 246 | + return null; | |
| 183 | 247 | } |
| 184 | 248 | |
| 185 | 249 | /** |
| 186 | 250 | * Find a shipping package by slug. |
| @@ -215,17 +279,15 @@ | ||
| 215 | 279 | } |
| 216 | 280 | |
| 217 | 281 | public static function getOrderStatuses() |
| 218 | 282 | { |
| 219 | - $statuses = apply_filters_deprecated('fluent-cart/order_statuses', [ | |
| 220 | - [ | |
| 221 | - 'on-hold' => __('On Hold', 'fluent-cart'), | |
| 222 | - 'processing' => __('Processing', 'fluent-cart'), | |
| 223 | - 'completed' => __('Completed', 'fluent-cart'), | |
| 224 | - //'archived' => __('Archived', 'fluent-cart'), | |
| 225 | - 'cancelled' => __('Cancelled', 'fluent-cart'), | |
| 226 | - ], [] | |
| 227 | - ], '1.3.16', 'fluent_cart/order_statuses', 'Use fluent_cart/order_statuses instead of fluent-cart/order_statuses.'); | |
| 283 | + $statuses = [ | |
| 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 | + ]; | |
| 228 | 290 | |
| 229 | 291 | return apply_filters('fluent_cart/order_statuses', $statuses, []); |
| 230 | 292 | } |
| 231 | 293 | |
| @@ -230,17 +292,15 @@ | ||
| 230 | 292 | } |
| 231 | 293 | |
| 232 | 294 | public static function getEditableOrderStatuses() |
| 233 | 295 | { |
| 234 | - $statuses = apply_filters_deprecated('fluent-cart/editable_order_statuses', [ | |
| 235 | - [ | |
| 236 | - 'on-hold' => __('On Hold', 'fluent-cart'), | |
| 237 | - 'processing' => __('Processing', 'fluent-cart'), | |
| 238 | - 'completed' => __('Completed', 'fluent-cart'), | |
| 239 | - // 'archived' => __('Archived', 'fluent-cart'), | |
| 240 | - 'cancelled' => __('Cancelled', 'fluent-cart') | |
| 241 | - ], [] | |
| 242 | - ], '1.3.16', 'fluent_cart/editable_order_statuses', 'Use fluent_cart/editable_order_statuses instead of fluent-cart/editable_order_statuses.'); | |
| 296 | + $statuses = [ | |
| 297 | + 'on-hold' => __('On Hold', 'fluent-cart'), | |
| 298 | + 'processing' => __('Processing', 'fluent-cart'), | |
| 299 | + 'completed' => __('Completed', 'fluent-cart'), | |
| 300 | + // 'archived' => __('Archived', 'fluent-cart'), | |
| 301 | + 'cancelled' => __('Cancelled', 'fluent-cart') | |
| 302 | + ]; | |
| 243 | 303 | |
| 244 | 304 | return apply_filters('fluent_cart/editable_order_statuses', $statuses, []); |
| 245 | 305 | } |
| 246 | 306 | |
| @@ -245,14 +305,12 @@ | ||
| 245 | 305 | } |
| 246 | 306 | |
| 247 | 307 | public static function getEditableCustomerStatuses() |
| 248 | 308 | { |
| 249 | - $statuses = apply_filters_deprecated('fluent-cart/editable_customer_statuses', [ | |
| 250 | - [ | |
| 251 | - 'active' => __('Active', 'fluent-cart'), | |
| 252 | - 'inactive' => __('Inactive', 'fluent-cart'), | |
| 253 | - ], [] | |
| 254 | - ], '1.3.16', 'fluent_cart/editable_customer_statuses', 'Use fluent_cart/editable_customer_statuses instead of fluent-cart/editable_customer_statuses.'); | |
| 309 | + $statuses = [ | |
| 310 | + 'active' => __('Active', 'fluent-cart'), | |
| 311 | + 'inactive' => __('Inactive', 'fluent-cart'), | |
| 312 | + ]; | |
| 255 | 313 | |
| 256 | 314 | return apply_filters('fluent_cart/editable_customer_statuses', $statuses, []); |
| 257 | 315 | } |
| 258 | 316 | |
| @@ -257,16 +315,14 @@ | ||
| 257 | 315 | } |
| 258 | 316 | |
| 259 | 317 | public static function getShippingStatuses() |
| 260 | 318 | { |
| 261 | - $statuses = apply_filters_deprecated('fluent-cart/shipping_statuses', [ | |
| 262 | - [ | |
| 263 | - 'unshipped' => __('Unshipped', 'fluent-cart'), | |
| 264 | - 'shipped' => __('Shipped', 'fluent-cart'), | |
| 265 | - 'delivered' => __('Delivered', 'fluent-cart'), | |
| 266 | - 'unshippable' => __('Unshippable', 'fluent-cart'), | |
| 267 | - ], [] | |
| 268 | - ], '1.3.16', 'fluent_cart/shipping_statuses', 'Use fluent_cart/shipping_statuses instead of fluent-cart/shipping_statuses.'); | |
| 319 | + $statuses = [ | |
| 320 | + 'unshipped' => __('Unshipped', 'fluent-cart'), | |
| 321 | + 'shipped' => __('Shipped', 'fluent-cart'), | |
| 322 | + 'delivered' => __('Delivered', 'fluent-cart'), | |
| 323 | + 'unshippable' => __('Unshippable', 'fluent-cart'), | |
| 324 | + ]; | |
| 269 | 325 | |
| 270 | 326 | return apply_filters('fluent_cart/shipping_statuses', $statuses, []); |
| 271 | 327 | } |
| 272 | 328 | |
| @@ -271,16 +327,14 @@ | ||
| 271 | 327 | } |
| 272 | 328 | |
| 273 | 329 | public static function getEditableShippingStatuses() |
| 274 | 330 | { |
| 275 | - $statuses = apply_filters_deprecated('fluent-cart/editable_order_statuses', [ | |
| 276 | - [ | |
| 277 | - 'unshipped' => __('Unshipped', 'fluent-cart'), | |
| 278 | - 'shipped' => __('Shipped', 'fluent-cart'), | |
| 279 | - 'delivered' => __('Delivered', 'fluent-cart'), | |
| 280 | - 'unshippable' => __('Unshippable', 'fluent-cart'), | |
| 281 | - ], [] | |
| 282 | - ], '1.3.16', 'fluent_cart/editable_shipping_statuses', 'Use fluent_cart/editable_shipping_statuses instead of fluent-cart/editable_order_statuses.'); | |
| 331 | + $statuses = [ | |
| 332 | + 'unshipped' => __('Unshipped', 'fluent-cart'), | |
| 333 | + 'shipped' => __('Shipped', 'fluent-cart'), | |
| 334 | + 'delivered' => __('Delivered', 'fluent-cart'), | |
| 335 | + 'unshippable' => __('Unshippable', 'fluent-cart'), | |
| 336 | + ]; | |
| 283 | 337 | |
| 284 | 338 | return apply_filters('fluent_cart/editable_shipping_statuses', $statuses, []); |
| 285 | 339 | } |
| 286 | 340 | |
| @@ -310,18 +364,16 @@ | ||
| 310 | 364 | } |
| 311 | 365 | |
| 312 | 366 | public static function getTransactionStatuses($withLabel = true) |
| 313 | 367 | { |
| 314 | - $statuses = apply_filters_deprecated('fluent-cart/transaction_statuses', [ | |
| 315 | - [ | |
| 316 | - 'pending' => __('Pending', 'fluent-cart'), | |
| 317 | - 'paid' => __('Paid', 'fluent-cart'), | |
| 318 | - 'require_capture' => __('Authorized (Require Capture)', 'fluent-cart'), | |
| 319 | - 'failed' => __('Failed', 'fluent-cart'), | |
| 320 | - 'refunded' => __('Refunded', 'fluent-cart'), | |
| 321 | - 'active' => __('Active', 'fluent-cart'), | |
| 322 | - ], [] | |
| 323 | - ], '1.3.16', 'fluent_cart/transaction_statuses', 'Use fluent_cart/transaction_statuses instead of fluent-cart/transaction_statuses.'); | |
| 368 | + $statuses = [ | |
| 369 | + 'pending' => __('Pending', 'fluent-cart'), | |
| 370 | + 'paid' => __('Paid', 'fluent-cart'), | |
| 371 | + 'require_capture' => __('Authorized (Require Capture)', 'fluent-cart'), | |
| 372 | + 'failed' => __('Failed', 'fluent-cart'), | |
| 373 | + 'refunded' => __('Refunded', 'fluent-cart'), | |
| 374 | + 'active' => __('Active', 'fluent-cart'), | |
| 375 | + ]; | |
| 324 | 376 | |
| 325 | 377 | $statuses = apply_filters('fluent_cart/transaction_statuses', $statuses, []); |
| 326 | 378 | |
| 327 | 379 | if ($withLabel) { |
| @@ -332,16 +384,14 @@ | ||
| 332 | 384 | } |
| 333 | 385 | |
| 334 | 386 | public static function getEditableTransactionStatuses($withLabel = true) |
| 335 | 387 | { |
| 336 | - $statuses = apply_filters_deprecated('fluent-cart/editable_transaction_statuses', [ | |
| 337 | - [ | |
| 338 | - 'pending' => __('Pending', 'fluent-cart'), | |
| 339 | - 'paid' => __('Paid', 'fluent-cart'), | |
| 340 | - 'failed' => __('Failed', 'fluent-cart'), | |
| 341 | - 'refunded' => __('Refunded', 'fluent-cart'), | |
| 342 | - ], [] | |
| 343 | - ], '1.3.16', 'fluent_cart/editable_transaction_statuses', 'Use fluent_cart/editable_transaction_statuses instead of fluent-cart/editable_transaction_statuses.'); | |
| 388 | + $statuses = [ | |
| 389 | + 'pending' => __('Pending', 'fluent-cart'), | |
| 390 | + 'paid' => __('Paid', 'fluent-cart'), | |
| 391 | + 'failed' => __('Failed', 'fluent-cart'), | |
| 392 | + 'refunded' => __('Refunded', 'fluent-cart'), | |
| 393 | + ]; | |
| 344 | 394 | |
| 345 | 395 | $statuses = apply_filters('fluent_cart/editable_transaction_statuses', $statuses, []); |
| 346 | 396 | |
| 347 | 397 | if ($withLabel) { |
| @@ -350,21 +400,8 @@ | ||
| 350 | 400 | |
| 351 | 401 | return array_keys($statuses); |
| 352 | 402 | } |
| 353 | 403 | |
| 354 | - public static function loadSpoutLib() | |
| 355 | - { | |
| 356 | - static $loaded; | |
| 357 | - | |
| 358 | - if ($loaded) { | |
| 359 | - return $loaded; | |
| 360 | - } | |
| 361 | - | |
| 362 | - require_once FLUENTCART_PLUGIN_PATH . 'app/Services/Libs/Spout/Autoloader/autoload.php'; | |
| 363 | - | |
| 364 | - return true; | |
| 365 | - } | |
| 366 | - | |
| 367 | 404 | public static function productStatuses($withLabel = true): array |
| 368 | 405 | { |
| 369 | 406 | $statues = [ |
| 370 | 407 | 'publish' => __('Publish', 'fluent-cart'), |
| @@ -471,9 +508,9 @@ | ||
| 471 | 508 | } |
| 472 | 509 | } |
| 473 | 510 | } |
| 474 | 511 | |
| 475 | - if($withTranslatedDigit) { | |
| 512 | + if ($withTranslatedDigit) { | |
| 476 | 513 | $amount = self::translateNumber($amount); |
| 477 | 514 | } |
| 478 | 515 | } |
| 479 | 516 | |
| @@ -515,8 +552,41 @@ | ||
| 515 | 552 | $amount = (int)round($amount); // Round to nearest integer, then cast |
| 516 | 553 | return $amount; |
| 517 | 554 | } |
| 518 | 555 | |
| 556 | + /** | |
| 557 | + * Normalize a value that is ALREADY in cents to a whole-cent int. | |
| 558 | + * | |
| 559 | + * Unlike toCent(), this does not scale — use it when the incoming value is a | |
| 560 | + * cents amount that may have arrived as a float. Clients computing cents in | |
| 561 | + * JavaScript send artifacts like 1998.9999999999998 for 1999, and a bare | |
| 562 | + * (int) cast truncates those, silently undercharging by a cent. Non-numeric | |
| 563 | + * input (including the null the request pipeline injects for omitted keys) | |
| 564 | + * normalizes to 0. | |
| 565 | + * | |
| 566 | + * Magnitudes beyond float's exact-integer range (2^53) are REJECTED, not | |
| 567 | + * coerced: is_numeric() accepts exponential notation like 1e19, and casting | |
| 568 | + * that float to int wraps to -8446744073709551616 — a VALID signed BIGINT — | |
| 569 | + * so the corrupt value would persist silently where an un-normalized float | |
| 570 | + * used to fail loudly at MySQL. No real cents amount approaches this bound. | |
| 571 | + */ | |
| 572 | + public static function roundCent($amount): int | |
| 573 | + { | |
| 574 | + if (!is_numeric($amount)) { | |
| 575 | + return 0; | |
| 576 | + } | |
| 577 | + | |
| 578 | + $amount = (float) $amount; | |
| 579 | + | |
| 580 | + if (!is_finite($amount) || $amount > 9.0e15 || $amount < -9.0e15) { | |
| 581 | + throw new \InvalidArgumentException( | |
| 582 | + 'Value is out of the representable cents range: ' . var_export($amount, true) | |
| 583 | + ); | |
| 584 | + } | |
| 585 | + | |
| 586 | + return (int) round($amount); | |
| 587 | + } | |
| 588 | + | |
| 519 | 589 | public static function toDecimalWithoutComma($amount) |
| 520 | 590 | { |
| 521 | 591 | |
| 522 | 592 | if (!is_numeric($amount)) { |
| @@ -539,10 +609,12 @@ | ||
| 539 | 609 | if (!$user) { |
| 540 | 610 | return null; |
| 541 | 611 | } |
| 542 | 612 | |
| 613 | + // Identity only — see CustomerResource::getCurrentCustomer() for why an | |
| 614 | + // email match must never reach a customer row. | |
| 543 | 615 | return Customer::query()->where('user_id', $user->ID) |
| 544 | - ->orWhere('email', $user->user_email) | |
| 616 | + ->orderBy('id', 'ASC') | |
| 545 | 617 | ->first(); |
| 546 | 618 | } |
| 547 | 619 | |
| 548 | 620 | /** |
| @@ -570,10 +642,9 @@ | ||
| 570 | 642 | } |
| 571 | 643 | |
| 572 | 644 | public static function getAvailableCurrencyList() |
| 573 | 645 | { |
| 574 | - $currencies = apply_filters_deprecated('fluent-cart/available_currencies', [ | |
| 575 | - [ | |
| 646 | + $currencies = [ | |
| 576 | 647 | 'BDT' => [ |
| 577 | 648 | "label" => __('Bangladeshi Taka', 'fluent-cart'), |
| 578 | 649 | "value" => 'BDT', |
| 579 | 650 | "symbol" => '৳', |
| @@ -587,10 +658,9 @@ | ||
| 587 | 658 | "label" => __('United Kingdom', 'fluent-cart'), |
| 588 | 659 | "value" => 'GBP', |
| 589 | 660 | "symbol" => '£', |
| 590 | 661 | ], |
| 591 | - ], [] | |
| 592 | - ], '1.3.16', 'fluent_cart/available_currencies', 'Use fluent_cart/available_currencies instead of fluent-cart/available_currencies.'); | |
| 662 | + ]; | |
| 593 | 663 | |
| 594 | 664 | return apply_filters('fluent_cart/available_currencies', $currencies, []); |
| 595 | 665 | } |
| 596 | 666 | |
| @@ -707,18 +777,25 @@ | ||
| 707 | 777 | } |
| 708 | 778 | |
| 709 | 779 | public static function getVariationTypes($withLabel = true) |
| 710 | 780 | { |
| 711 | - $statues = [ | |
| 712 | - 'simple' => __('Simple', 'fluent-cart'), | |
| 713 | - 'simple_variations' => __('Simple Variation', 'fluent-cart'), | |
| 781 | + // advanced_variations is advertised in free too so the variation-type | |
| 782 | + // dropdown can offer it (shown Pro-locked with a crown and an upgrade | |
| 783 | + // modal while Pro is inactive). Pro re-registers the same key via the | |
| 784 | + // filter below when active. | |
| 785 | + $types = [ | |
| 786 | + 'simple' => __('Simple', 'fluent-cart'), | |
| 787 | + 'simple_variations' => __('Simple Variations', 'fluent-cart'), | |
| 788 | + 'advanced_variations' => __('Advanced Variations', 'fluent-cart'), | |
| 714 | 789 | ]; |
| 715 | 790 | |
| 791 | + $types = apply_filters('fluent_cart/variation_types', $types); | |
| 792 | + | |
| 716 | 793 | if ($withLabel) { |
| 717 | - return $statues; | |
| 794 | + return $types; | |
| 718 | 795 | } |
| 719 | 796 | |
| 720 | - return array_keys($statues); | |
| 797 | + return array_keys($types); | |
| 721 | 798 | } |
| 722 | 799 | |
| 723 | 800 | public static function isValueEncrypted($raw_value) |
| 724 | 801 | { |
| @@ -930,15 +1007,13 @@ | ||
| 930 | 1007 | } |
| 931 | 1008 | |
| 932 | 1009 | public static function getCouponStatuses() |
| 933 | 1010 | { |
| 934 | - $statuses = apply_filters_deprecated('fluent-cart/coupon_statuses', [ | |
| 935 | - [ | |
| 936 | - 'active' => __('Active', 'fluent-cart'), | |
| 937 | - 'expired' => __('Expired', 'fluent-cart'), | |
| 938 | - 'disabled' => __('Disabled', 'fluent-cart'), | |
| 939 | - ], [] | |
| 940 | - ], '1.3.16', 'fluent_cart/coupon_statuses', 'Use fluent_cart/coupon_statuses instead of fluent-cart/coupon_statuses.'); | |
| 1011 | + $statuses = [ | |
| 1012 | + 'active' => __('Active', 'fluent-cart'), | |
| 1013 | + 'expired' => __('Expired', 'fluent-cart'), | |
| 1014 | + 'disabled' => __('Disabled', 'fluent-cart'), | |
| 1015 | + ]; | |
| 941 | 1016 | |
| 942 | 1017 | return apply_filters('fluent_cart/coupon_statuses', $statuses, []); |
| 943 | 1018 | } |
| 944 | 1019 | |
| @@ -975,9 +1050,9 @@ | ||
| 975 | 1050 | $intervalOptions = static::getAvailableSubscriptionIntervalMaps(); |
| 976 | 1051 | |
| 977 | 1052 | // Normalize / defaults |
| 978 | 1053 | $trialDays = $data['trial_days'] ?? 0; |
| 979 | - $interval = (string)($data['interval'] ? $data['interval'] : 'monthly'); | |
| 1054 | + $interval = (string)($data['interval'] ? $data['interval'] : 'monthly'); | |
| 980 | 1055 | |
| 981 | 1056 | $unit = ''; |
| 982 | 1057 | if (isset($intervalOptions[$interval])) { |
| 983 | 1058 | $unit = $intervalOptions[$interval]; |
| @@ -1131,9 +1206,9 @@ | ||
| 1131 | 1206 | return $unit; |
| 1132 | 1207 | } |
| 1133 | 1208 | } |
| 1134 | 1209 | |
| 1135 | - public static function generateSubscriptionInfo($otherInfo, $itemPrice): ?string | |
| 1210 | + public static function generateSubscriptionInfo($otherInfo, $itemPrice, $currencyCode = null): ?string | |
| 1136 | 1211 | { |
| 1137 | 1212 | // Convert to array only if it's an object |
| 1138 | 1213 | if (is_object($otherInfo)) { |
| 1139 | 1214 | $otherInfo = json_decode(json_encode($otherInfo), true); |
| @@ -1138,14 +1213,14 @@ | ||
| 1138 | 1213 | if (is_object($otherInfo)) { |
| 1139 | 1214 | $otherInfo = json_decode(json_encode($otherInfo), true); |
| 1140 | 1215 | } |
| 1141 | 1216 | |
| 1142 | - $price = self::toDecimal($itemPrice); | |
| 1217 | + $price = self::toDecimal($itemPrice, true, $currencyCode); | |
| 1143 | 1218 | $recurringDiscountAmount = Arr::get($otherInfo, 'recurring_discounts.amount', 0); |
| 1144 | 1219 | |
| 1145 | 1220 | if ($recurringDiscountAmount) { |
| 1146 | 1221 | $newRecurringAmount = $itemPrice - $recurringDiscountAmount; |
| 1147 | - $price = "<del>" . $price . "</del> " . self::toDecimal($newRecurringAmount); | |
| 1222 | + $price = "<del>" . $price . "</del> " . self::toDecimal($newRecurringAmount, true, $currencyCode); | |
| 1148 | 1223 | } |
| 1149 | 1224 | |
| 1150 | 1225 | $repeatInterval = Arr::get($otherInfo, 'repeat_interval', ''); |
| 1151 | 1226 | $occurrence = (int)Arr::get($otherInfo, 'times', 0); |
| @@ -1155,19 +1230,19 @@ | ||
| 1155 | 1230 | $intervalUnit = ''; |
| 1156 | 1231 | if (isset($intervalOptions[$repeatInterval])) { |
| 1157 | 1232 | $intervalUnit = $intervalOptions[$repeatInterval]; |
| 1158 | 1233 | } else if ($repeatInterval) { |
| 1159 | - $intervalOptions = static::getAvailableSubscriptionIntervalOptions(); | |
| 1160 | - foreach ($intervalOptions as $option) { | |
| 1161 | - if ($option['value'] === $repeatInterval) { | |
| 1162 | - $intervalUnit = strtolower($option['label']); | |
| 1163 | - break; | |
| 1164 | - } | |
| 1234 | + $intervalOptions = static::getAvailableSubscriptionIntervalOptions(); | |
| 1235 | + foreach ($intervalOptions as $option) { | |
| 1236 | + if ($option['value'] === $repeatInterval) { | |
| 1237 | + $intervalUnit = strtolower($option['label']); | |
| 1238 | + break; | |
| 1165 | 1239 | } |
| 1240 | + } | |
| 1166 | 1241 | |
| 1167 | - if (!$intervalUnit) { | |
| 1168 | - $intervalUnit = ucwords(str_replace(['_', '-'], ' ', $repeatInterval)); | |
| 1169 | - } | |
| 1242 | + if (!$intervalUnit) { | |
| 1243 | + $intervalUnit = ucwords(str_replace(['_', '-'], ' ', $repeatInterval)); | |
| 1244 | + } | |
| 1170 | 1245 | } |
| 1171 | 1246 | |
| 1172 | 1247 | $intervalLabel = Helper::getTranslatedIntervalUnit($intervalUnit); |
| 1173 | 1248 | |
| @@ -1172,9 +1247,9 @@ | ||
| 1172 | 1247 | $intervalLabel = Helper::getTranslatedIntervalUnit($intervalUnit); |
| 1173 | 1248 | |
| 1174 | 1249 | $interval = $intervalUnit |
| 1175 | 1250 | ? sprintf( |
| 1176 | - /* translators: %s is the interval (e.g., day, week, month, quarter, half_year, year) */ | |
| 1251 | + /* translators: %s is the interval (e.g., day, week, month, quarter, half_year, year) */ | |
| 1177 | 1252 | __('per %s', 'fluent-cart'), |
| 1178 | 1253 | $intervalLabel |
| 1179 | 1254 | ) |
| 1180 | 1255 | : ''; |
| @@ -1204,10 +1279,152 @@ | ||
| 1204 | 1279 | |
| 1205 | 1280 | return !empty($otherInfo) ? $paymentInfo : null; |
| 1206 | 1281 | } |
| 1207 | 1282 | |
| 1208 | - public static function generateSetupFeeInfo($otherInfo): ?string | |
| 1283 | + /** | |
| 1284 | + * Billing-cycle text for a subscription with a config-defined schedule | |
| 1285 | + * (see SubscriptionHelper::getBillingSchedule()) — cadences the | |
| 1286 | + * billing_interval enum cannot express, e.g. "$100 per 3 years on Aug 19". | |
| 1287 | + * Subscriptions without a schedule keep generateSubscriptionInfo(). | |
| 1288 | + */ | |
| 1289 | + public static function generateScheduleSubscriptionInfo(array $schedule, $otherInfo, $itemPrice, $currencyCode = null): ?string | |
| 1209 | 1290 | { |
| 1291 | + if (is_object($otherInfo)) { | |
| 1292 | + $otherInfo = json_decode(json_encode($otherInfo), true); | |
| 1293 | + } | |
| 1294 | + | |
| 1295 | + $count = max(1, (int) Arr::get($schedule, 'interval', 1)); | |
| 1296 | + | |
| 1297 | + switch (Arr::get($schedule, 'period')) { | |
| 1298 | + case 'day': | |
| 1299 | + $unitLabel = _n('day', 'days', $count, 'fluent-cart'); | |
| 1300 | + break; | |
| 1301 | + case 'week': | |
| 1302 | + $unitLabel = _n('week', 'weeks', $count, 'fluent-cart'); | |
| 1303 | + break; | |
| 1304 | + case 'month': | |
| 1305 | + $unitLabel = _n('month', 'months', $count, 'fluent-cart'); | |
| 1306 | + break; | |
| 1307 | + case 'year': | |
| 1308 | + $unitLabel = _n('year', 'years', $count, 'fluent-cart'); | |
| 1309 | + break; | |
| 1310 | + default: | |
| 1311 | + return self::generateSubscriptionInfo($otherInfo, $itemPrice, $currencyCode); | |
| 1312 | + } | |
| 1313 | + | |
| 1314 | + $price = self::toDecimal($itemPrice, true, $currencyCode); | |
| 1315 | + $recurringDiscountAmount = Arr::get($otherInfo, 'recurring_discounts.amount', 0); | |
| 1316 | + | |
| 1317 | + if ($recurringDiscountAmount) { | |
| 1318 | + $newRecurringAmount = $itemPrice - $recurringDiscountAmount; | |
| 1319 | + $price = "<del>" . $price . "</del> " . self::toDecimal($newRecurringAmount, true, $currencyCode); | |
| 1320 | + } | |
| 1321 | + | |
| 1322 | + $interval = $count === 1 | |
| 1323 | + ? sprintf( | |
| 1324 | + /* translators: %s is the interval unit (e.g., day, week, month, year) */ | |
| 1325 | + __('per %s', 'fluent-cart'), | |
| 1326 | + $unitLabel | |
| 1327 | + ) | |
| 1328 | + : sprintf( | |
| 1329 | + /* translators: %1$d is the count number, %2$s is the plural unit name (e.g., days, months, years) */ | |
| 1330 | + __('per %1$d %2$s', 'fluent-cart'), | |
| 1331 | + $count, | |
| 1332 | + $unitLabel | |
| 1333 | + ); | |
| 1334 | + | |
| 1335 | + if ($anchorText = self::getScheduleAnchorText($schedule['period'], Arr::get($schedule, 'anchor', []))) { | |
| 1336 | + $interval .= ' ' . $anchorText; | |
| 1337 | + } | |
| 1338 | + | |
| 1339 | + $occurrence = (int) Arr::get($otherInfo, 'times', 0); | |
| 1340 | + | |
| 1341 | + if (empty($occurrence)) { | |
| 1342 | + return sprintf( | |
| 1343 | + /* translators: %1$s is the price, %2$s is the interval, %3$s is "until cancel" text */ | |
| 1344 | + __('%1$s %2$s %3$s', 'fluent-cart'), | |
| 1345 | + $price, | |
| 1346 | + $interval, | |
| 1347 | + __('until cancel', 'fluent-cart') | |
| 1348 | + ); | |
| 1349 | + } | |
| 1350 | + | |
| 1351 | + return sprintf( | |
| 1352 | + /* translators: %1$s is the price, %2$s is the interval, %3$s is the occurrence count, %4$s is "cycle(s)" */ | |
| 1353 | + __('%1$s %2$s, for %3$s %4$s', 'fluent-cart'), | |
| 1354 | + $price, | |
| 1355 | + $interval, | |
| 1356 | + $occurrence, | |
| 1357 | + _n('cycle', 'cycles', $occurrence, 'fluent-cart') | |
| 1358 | + ); | |
| 1359 | + } | |
| 1360 | + | |
| 1361 | + /** | |
| 1362 | + * Human-readable billing anchor, e.g. "on Friday", "on the 10th", | |
| 1363 | + * "on the last day", "on Aug 19". Anchor day 31 encodes "last day of | |
| 1364 | + * the month" (see SubscriptionHelper::getBillingSchedule()). | |
| 1365 | + */ | |
| 1366 | + private static function getScheduleAnchorText(string $period, $anchor): string | |
| 1367 | + { | |
| 1368 | + if (!is_array($anchor) || !$anchor) { | |
| 1369 | + return ''; | |
| 1370 | + } | |
| 1371 | + | |
| 1372 | + if ($period === 'week' && !empty($anchor['weekday'])) { | |
| 1373 | + $weekdays = [ | |
| 1374 | + 1 => __('Monday', 'fluent-cart'), | |
| 1375 | + 2 => __('Tuesday', 'fluent-cart'), | |
| 1376 | + 3 => __('Wednesday', 'fluent-cart'), | |
| 1377 | + 4 => __('Thursday', 'fluent-cart'), | |
| 1378 | + 5 => __('Friday', 'fluent-cart'), | |
| 1379 | + 6 => __('Saturday', 'fluent-cart'), | |
| 1380 | + 7 => __('Sunday', 'fluent-cart'), | |
| 1381 | + ]; | |
| 1382 | + | |
| 1383 | + if (isset($weekdays[$anchor['weekday']])) { | |
| 1384 | + /* translators: %s is a weekday name, e.g. "on Friday" */ | |
| 1385 | + return sprintf(__('on %s', 'fluent-cart'), $weekdays[$anchor['weekday']]); | |
| 1386 | + } | |
| 1387 | + | |
| 1388 | + return ''; | |
| 1389 | + } | |
| 1390 | + | |
| 1391 | + if ($period === 'month' && !empty($anchor['day'])) { | |
| 1392 | + $day = (int) $anchor['day']; | |
| 1393 | + | |
| 1394 | + if ($day === 31) { | |
| 1395 | + return __('on the last day', 'fluent-cart'); | |
| 1396 | + } | |
| 1397 | + | |
| 1398 | + /* translators: %s is an ordinal day of month, e.g. "on the 10th" */ | |
| 1399 | + return sprintf(__('on the %s', 'fluent-cart'), gmdate('jS', gmmktime(12, 0, 0, 1, $day, 2001))); | |
| 1400 | + } | |
| 1401 | + | |
| 1402 | + if ($period === 'year' && (!empty($anchor['day']) || !empty($anchor['month']))) { | |
| 1403 | + $day = (int) Arr::get($anchor, 'day', 0); | |
| 1404 | + $month = (int) Arr::get($anchor, 'month', 0); | |
| 1405 | + | |
| 1406 | + if ($month && $day) { | |
| 1407 | + /* translators: %s is a date, e.g. "on Aug 19" */ | |
| 1408 | + // wp_date(), not gmdate(): gmdate() has no locale, so this month | |
| 1409 | + // name stayed English inside an otherwise translated sentence. | |
| 1410 | + return sprintf(__('on %s', 'fluent-cart'), wp_date('M', gmmktime(12, 0, 0, $month, 1, 2001), new \DateTimeZone('UTC')) . ' ' . $day); | |
| 1411 | + } | |
| 1412 | + | |
| 1413 | + if ($month) { | |
| 1414 | + /* translators: %s is a month name, e.g. "in August" */ | |
| 1415 | + return sprintf(__('in %s', 'fluent-cart'), wp_date('F', gmmktime(12, 0, 0, $month, 1, 2001), new \DateTimeZone('UTC'))); | |
| 1416 | + } | |
| 1417 | + | |
| 1418 | + /* translators: %s is an ordinal day of month, e.g. "on the 10th" */ | |
| 1419 | + return sprintf(__('on the %s', 'fluent-cart'), gmdate('jS', gmmktime(12, 0, 0, 1, $day, 2001))); | |
| 1420 | + } | |
| 1421 | + | |
| 1422 | + return ''; | |
| 1423 | + } | |
| 1424 | + | |
| 1425 | + public static function generateSetupFeeInfo($otherInfo, $asArray = false) | |
| 1426 | + { | |
| 1210 | 1427 | // Convert to array if it's an object |
| 1211 | 1428 | if (is_object($otherInfo)) { |
| 1212 | 1429 | $otherInfo = json_decode(json_encode($otherInfo), true); |
| 1213 | 1430 | } |
| @@ -1222,14 +1439,33 @@ | ||
| 1222 | 1439 | |
| 1223 | 1440 | |
| 1224 | 1441 | if ($originalSetupFee = Arr::get($otherInfo, 'original_signup_fee', 0)) { |
| 1225 | 1442 | if ($fee != $originalSetupFee) { |
| 1226 | - return __('Adjusted setup fee', 'fluent-cart') . CurrencySettings::getPriceHtml($fee, null, true, true); | |
| 1443 | + $title = __('Adjusted setup fee', 'fluent-cart'); | |
| 1444 | + $formattedAmount = CurrencySettings::getPriceHtml($fee, null, true, true); | |
| 1445 | + | |
| 1446 | + if ($asArray) { | |
| 1447 | + return [ | |
| 1448 | + 'signup_fee_name' => $title, | |
| 1449 | + 'signup_fee' => $fee, | |
| 1450 | + 'signup_fee_formatted' => $formattedAmount, | |
| 1451 | + ]; | |
| 1452 | + } | |
| 1453 | + return $title . $formattedAmount; | |
| 1227 | 1454 | } |
| 1228 | 1455 | } |
| 1229 | 1456 | |
| 1457 | + $formattedAmount = CurrencySettings::getPriceHtml($fee, null, true, true); | |
| 1458 | + if ($asArray) { | |
| 1459 | + return [ | |
| 1460 | + 'signup_fee_name' => $signupFeeName, | |
| 1461 | + 'signup_fee' => $fee, | |
| 1462 | + 'signup_fee_formatted' => $formattedAmount, | |
| 1463 | + ]; | |
| 1464 | + } | |
| 1230 | 1465 | |
| 1231 | - return $signupFeeName . ' ' . CurrencySettings::getPriceHtml($fee, null, true, true); | |
| 1466 | + | |
| 1467 | + return $signupFeeName . ' ' . $formattedAmount; | |
| 1232 | 1468 | } |
| 1233 | 1469 | |
| 1234 | 1470 | public static function generateTrialInfo($otherInfo) |
| 1235 | 1471 | { |
| @@ -1251,9 +1487,8 @@ | ||
| 1251 | 1487 | |
| 1252 | 1488 | public static function getCountryList(): array |
| 1253 | 1489 | { |
| 1254 | 1490 | $options = App::getInstance('localization')->countriesOptions(); |
| 1255 | - $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.'); | |
| 1256 | 1491 | |
| 1257 | 1492 | return apply_filters('fluent_cart/util/countries', $options, []); |
| 1258 | 1493 | } |
| 1259 | 1494 | |
| @@ -1500,13 +1735,13 @@ | ||
| 1500 | 1735 | /** |
| 1501 | 1736 | * Get the current user Model. |
| 1502 | 1737 | * @return User|\FluentCart\Framework\Database\Orm\Builder|\FluentCart\Framework\Database\Orm\Builder[]|\FluentCart\Framework\Database\Orm\Collection|\FluentCart\Framework\Database\Orm\Model|null |
| 1503 | 1738 | */ |
| 1504 | - public static function getCurrentUser() | |
| 1739 | + public static function getCurrentUser($refresh = false) | |
| 1505 | 1740 | { |
| 1506 | 1741 | static $user = false; |
| 1507 | 1742 | |
| 1508 | - if ($user !== false) { | |
| 1743 | + if (!$refresh && $user !== false) { | |
| 1509 | 1744 | return $user; |
| 1510 | 1745 | } |
| 1511 | 1746 | |
| 1512 | 1747 | $userId = get_current_user_id(); |
| @@ -1597,12 +1832,14 @@ | ||
| 1597 | 1832 | |
| 1598 | 1833 | public static function humanIntervalMaps($interval = '') |
| 1599 | 1834 | { |
| 1600 | 1835 | $intervals = [ |
| 1601 | - 'daily' => 'day', | |
| 1602 | - 'weekly' => 'week', | |
| 1603 | - 'monthly' => 'month', | |
| 1604 | - 'yearly' => 'year' | |
| 1836 | + 'daily' => __('day', 'fluent-cart'), | |
| 1837 | + 'weekly' => __('week', 'fluent-cart'), | |
| 1838 | + 'monthly' => __('month', 'fluent-cart'), | |
| 1839 | + 'quarterly' => __('quarter', 'fluent-cart'), | |
| 1840 | + 'half_yearly' => __('six month', 'fluent-cart'), | |
| 1841 | + 'yearly' => __('year', 'fluent-cart'), | |
| 1605 | 1842 | ]; |
| 1606 | 1843 | |
| 1607 | 1844 | return Arr::get($intervals, $interval); |
| 1608 | 1845 | } |
| @@ -1613,35 +1850,35 @@ | ||
| 1613 | 1850 | public static function getAvailableSubscriptionIntervalOptions(): array |
| 1614 | 1851 | { |
| 1615 | 1852 | $intervals = [ |
| 1616 | 1853 | [ |
| 1617 | - 'label' => __('Yearly', 'fluent-cart'), | |
| 1618 | - 'value' => 'yearly', | |
| 1854 | + 'label' => __('Yearly', 'fluent-cart'), | |
| 1855 | + 'value' => 'yearly', | |
| 1619 | 1856 | 'map_value' => 'year', |
| 1620 | 1857 | ], |
| 1621 | 1858 | [ |
| 1622 | - 'label' => __('Half Yearly', 'fluent-cart'), | |
| 1623 | - 'value' => 'half_yearly', | |
| 1859 | + 'label' => __('Half Yearly', 'fluent-cart'), | |
| 1860 | + 'value' => 'half_yearly', | |
| 1624 | 1861 | 'map_value' => 'half_year', |
| 1625 | 1862 | ], |
| 1626 | 1863 | [ |
| 1627 | - 'label' => __('Quarterly', 'fluent-cart'), | |
| 1628 | - 'value' => 'quarterly', | |
| 1864 | + 'label' => __('Quarterly', 'fluent-cart'), | |
| 1865 | + 'value' => 'quarterly', | |
| 1629 | 1866 | 'map_value' => 'quarter', |
| 1630 | 1867 | ], |
| 1631 | 1868 | [ |
| 1632 | - 'label' => __('Monthly', 'fluent-cart'), | |
| 1633 | - 'value' => 'monthly', | |
| 1869 | + 'label' => __('Monthly', 'fluent-cart'), | |
| 1870 | + 'value' => 'monthly', | |
| 1634 | 1871 | 'map_value' => 'month', |
| 1635 | 1872 | ], |
| 1636 | 1873 | [ |
| 1637 | - 'label' => __('Weekly', 'fluent-cart'), | |
| 1638 | - 'value' => 'weekly', | |
| 1874 | + 'label' => __('Weekly', 'fluent-cart'), | |
| 1875 | + 'value' => 'weekly', | |
| 1639 | 1876 | 'map_value' => 'week', |
| 1640 | 1877 | ], |
| 1641 | 1878 | [ |
| 1642 | - 'label' => __('Daily', 'fluent-cart'), | |
| 1643 | - 'value' => 'daily', | |
| 1879 | + 'label' => __('Daily', 'fluent-cart'), | |
| 1880 | + 'value' => 'daily', | |
| 1644 | 1881 | 'map_value' => 'day', |
| 1645 | 1882 | ] |
| 1646 | 1883 | ]; |
| 1647 | 1884 | |
| @@ -1681,10 +1918,10 @@ | ||
| 1681 | 1918 | $intervalInDays = static::subscriptionIntervalInDays($repeatInterval); |
| 1682 | 1919 | |
| 1683 | 1920 | $maxTrialDaysAllowed = apply_filters('fluent_cart/max_trial_days_allowed', 365, [ |
| 1684 | 1921 | 'existing_trial_days' => $trialDays, |
| 1685 | - 'repeat_interval' => $repeatInterval, | |
| 1686 | - 'interval_in_days' => $intervalInDays, | |
| 1922 | + 'repeat_interval' => $repeatInterval, | |
| 1923 | + 'interval_in_days' => $intervalInDays, | |
| 1687 | 1924 | ]); |
| 1688 | 1925 | |
| 1689 | 1926 | 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 |
| 1690 | 1927 | |
| @@ -1691,26 +1928,9 @@ | ||
| 1691 | 1928 | } |
| 1692 | 1929 | |
| 1693 | 1930 | public static function subscriptionIntervalInDays($interval) |
| 1694 | 1931 | { |
| 1695 | - switch ($interval) { | |
| 1696 | - case 'daily': | |
| 1697 | - return 1; | |
| 1698 | - case 'weekly': | |
| 1699 | - return 7; | |
| 1700 | - case 'monthly': | |
| 1701 | - return 30; | |
| 1702 | - case 'quarterly': | |
| 1703 | - return 90; | |
| 1704 | - case 'half_yearly': | |
| 1705 | - return 182; | |
| 1706 | - case 'yearly': | |
| 1707 | - return 365; | |
| 1708 | - default: | |
| 1709 | - return apply_filters('fluent_cart/subscription_interval_in_days', 0, [ | |
| 1710 | - 'interval' => $interval, | |
| 1711 | - ]); | |
| 1712 | - } | |
| 1932 | + return PaymentHelper::getIntervalDays($interval); | |
| 1713 | 1933 | } |
| 1714 | 1934 | |
| 1715 | 1935 | public static function parseTermIdsForFilter($filters): array |
| 1716 | 1936 | { |
| @@ -1765,12 +1985,12 @@ | ||
| 1765 | 1985 | { |
| 1766 | 1986 | $allChildVariants = Arr::pluck($variants, 'other_info.bundle_child_ids'); |
| 1767 | 1987 | |
| 1768 | 1988 | $allChildVariants = array_unique(Arr::flatten($allChildVariants)); |
| 1769 | - $allChildVariants = Arr::except( | |
| 1989 | + $allChildVariants = array_values(array_diff( | |
| 1770 | 1990 | $allChildVariants, |
| 1771 | 1991 | Arr::pluck($variants, 'id') |
| 1772 | - ); | |
| 1992 | + )); | |
| 1773 | 1993 | $allChildVariants = array_filter($allChildVariants); |
| 1774 | 1994 | $childVariants = ProductVariation::query() |
| 1775 | 1995 | ->whereIn('id', $allChildVariants) |
| 1776 | 1996 | ->with('product:ID,post_title') |
| @@ -1823,10 +2043,10 @@ | ||
| 1823 | 2043 | $digits = explode('_', $numericSystem); |
| 1824 | 2044 | |
| 1825 | 2045 | return strtr( |
| 1826 | 2046 | (string)$number, |
| 1827 | - array_combine(range(0,9), | |
| 1828 | - $digits) | |
| 2047 | + array_combine(range(0, 9), | |
| 2048 | + $digits) | |
| 1829 | 2049 | ); |
| 1830 | 2050 | } |
| 1831 | 2051 | |
| 1832 | 2052 | public static function isModalCheckoutEnabled(): bool |
| @@ -1865,6 +2085,32 @@ | ||
| 1865 | 2085 | } |
| 1866 | 2086 | } |
| 1867 | 2087 | |
| 1868 | 2088 | return $default; |
| 2089 | + } | |
| 2090 | + | |
| 2091 | + public static function formatTaxRatePercent(float $rate): string | |
| 2092 | + { | |
| 2093 | + $formatted = number_format($rate, 4, '.', ''); | |
| 2094 | + if (strpos($formatted, '.') !== false) { | |
| 2095 | + $formatted = rtrim($formatted, '0'); | |
| 2096 | + $formatted = rtrim($formatted, '.'); | |
| 2097 | + } | |
| 2098 | + return $formatted; | |
| 2099 | + } | |
| 2100 | + | |
| 2101 | + /** | |
| 2102 | + * Returns the tax label for order-level tax rows (tax_total, not per-item). | |
| 2103 | + * For mixed orders, indicates tax varies per item. | |
| 2104 | + * | |
| 2105 | + * @param \FluentCart\App\Models\Order $order | |
| 2106 | + * @return string | |
| 2107 | + */ | |
| 2108 | + public static function getOrderTaxLabel($order) { | |
| 2109 | + if ((int) $order->tax_behavior === 3) { | |
| 2110 | + return esc_html__('(Varies)', 'fluent-cart'); | |
| 2111 | + } | |
| 2112 | + return (int) $order->tax_behavior === 2 | |
| 2113 | + ? esc_html__('(Included)', 'fluent-cart') | |
| 2114 | + : esc_html__('(Excluded)', 'fluent-cart'); | |
| 1869 | 2115 | } |
| 1870 | 2116 | } |