PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.5
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.5
1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk All 48 releases
← All changes | app/Helpers/Helper.php +248 -119 1.6.0 → 1.6.5 View file →
@@ -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;
@@ -278,17 +279,15 @@
278 279 }
279 280
280 281 public static function getOrderStatuses()
281 282 {
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.');
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 + ];
291 290
292 291 return apply_filters('fluent_cart/order_statuses', $statuses, []);
293 292 }
294 293
@@ -293,17 +292,15 @@
293 292 }
294 293
295 294 public static function getEditableOrderStatuses()
296 295 {
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.');
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 + ];
306 303
307 304 return apply_filters('fluent_cart/editable_order_statuses', $statuses, []);
308 305 }
309 306
@@ -308,14 +305,12 @@
308 305 }
309 306
310 307 public static function getEditableCustomerStatuses()
311 308 {
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.');
309 + $statuses = [
310 + 'active' => __('Active', 'fluent-cart'),
311 + 'inactive' => __('Inactive', 'fluent-cart'),
312 + ];
318 313
319 314 return apply_filters('fluent_cart/editable_customer_statuses', $statuses, []);
320 315 }
321 316
@@ -320,16 +315,14 @@
320 315 }
321 316
322 317 public static function getShippingStatuses()
323 318 {
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.');
319 + $statuses = [
320 + 'unshipped' => __('Unshipped', 'fluent-cart'),
321 + 'shipped' => __('Shipped', 'fluent-cart'),
322 + 'delivered' => __('Delivered', 'fluent-cart'),
323 + 'unshippable' => __('Unshippable', 'fluent-cart'),
324 + ];
332 325
333 326 return apply_filters('fluent_cart/shipping_statuses', $statuses, []);
334 327 }
335 328
@@ -334,16 +327,14 @@
334 327 }
335 328
336 329 public static function getEditableShippingStatuses()
337 330 {
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.');
331 + $statuses = [
332 + 'unshipped' => __('Unshipped', 'fluent-cart'),
333 + 'shipped' => __('Shipped', 'fluent-cart'),
334 + 'delivered' => __('Delivered', 'fluent-cart'),
335 + 'unshippable' => __('Unshippable', 'fluent-cart'),
336 + ];
346 337
347 338 return apply_filters('fluent_cart/editable_shipping_statuses', $statuses, []);
348 339 }
349 340
@@ -373,18 +364,16 @@
373 364 }
374 365
375 366 public static function getTransactionStatuses($withLabel = true)
376 367 {
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.');
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 + ];
387 376
388 377 $statuses = apply_filters('fluent_cart/transaction_statuses', $statuses, []);
389 378
390 379 if ($withLabel) {
@@ -395,16 +384,14 @@
395 384 }
396 385
397 386 public static function getEditableTransactionStatuses($withLabel = true)
398 387 {
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.');
388 + $statuses = [
389 + 'pending' => __('Pending', 'fluent-cart'),
390 + 'paid' => __('Paid', 'fluent-cart'),
391 + 'failed' => __('Failed', 'fluent-cart'),
392 + 'refunded' => __('Refunded', 'fluent-cart'),
393 + ];
407 394
408 395 $statuses = apply_filters('fluent_cart/editable_transaction_statuses', $statuses, []);
409 396
410 397 if ($withLabel) {
@@ -413,21 +400,8 @@
413 400
414 401 return array_keys($statuses);
415 402 }
416 403
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 404 public static function productStatuses($withLabel = true): array
431 405 {
432 406 $statues = [
433 407 'publish' => __('Publish', 'fluent-cart'),
@@ -578,8 +552,41 @@
578 552 $amount = (int)round($amount); // Round to nearest integer, then cast
579 553 return $amount;
580 554 }
581 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 +
582 589 public static function toDecimalWithoutComma($amount)
583 590 {
584 591
585 592 if (!is_numeric($amount)) {
@@ -602,10 +609,12 @@
602 609 if (!$user) {
603 610 return null;
604 611 }
605 612
613 + // Identity only — see CustomerResource::getCurrentCustomer() for why an
614 + // email match must never reach a customer row.
606 615 return Customer::query()->where('user_id', $user->ID)
607 - ->orWhere('email', $user->user_email)
616 + ->orderBy('id', 'ASC')
608 617 ->first();
609 618 }
610 619
611 620 /**
@@ -633,27 +642,25 @@
633 642 }
634 643
635 644 public static function getAvailableCurrencyList()
636 645 {
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.');
646 + $currencies = [
647 + 'BDT' => [
648 + "label" => __('Bangladeshi Taka', 'fluent-cart'),
649 + "value" => 'BDT',
650 + "symbol" => '৳',
651 + ],
652 + 'USD' => [
653 + "label" => __('United State Dollar', 'fluent-cart'),
654 + "value" => 'USD',
655 + "symbol" => '$',
656 + ],
657 + 'GBP' => [
658 + "label" => __('United Kingdom', 'fluent-cart'),
659 + "value" => 'GBP',
660 + "symbol" => '£',
661 + ],
662 + ];
656 663
657 664 return apply_filters('fluent_cart/available_currencies', $currencies, []);
658 665 }
659 666
@@ -1000,15 +1007,13 @@
1000 1007 }
1001 1008
1002 1009 public static function getCouponStatuses()
1003 1010 {
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 + $statuses = [
1012 + 'active' => __('Active', 'fluent-cart'),
1013 + 'expired' => __('Expired', 'fluent-cart'),
1014 + 'disabled' => __('Disabled', 'fluent-cart'),
1015 + ];
1011 1016
1012 1017 return apply_filters('fluent_cart/coupon_statuses', $statuses, []);
1013 1018 }
1014 1019
@@ -1274,8 +1279,150 @@
1274 1279
1275 1280 return !empty($otherInfo) ? $paymentInfo : null;
1276 1281 }
1277 1282
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
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 +
1278 1425 public static function generateSetupFeeInfo($otherInfo, $asArray = false)
1279 1426 {
1280 1427 // Convert to array if it's an object
1281 1428 if (is_object($otherInfo)) {
@@ -1340,9 +1487,8 @@
1340 1487
1341 1488 public static function getCountryList(): array
1342 1489 {
1343 1490 $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 1491
1346 1492 return apply_filters('fluent_cart/util/countries', $options, []);
1347 1493 }
1348 1494
@@ -1782,26 +1928,9 @@
1782 1928 }
1783 1929
1784 1930 public static function subscriptionIntervalInDays($interval)
1785 1931 {
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 - }
1932 + return PaymentHelper::getIntervalDays($interval);
1804 1933 }
1805 1934
1806 1935 public static function parseTermIdsForFilter($filters): array
1807 1936 {
@@ -1856,12 +1985,12 @@
1856 1985 {
1857 1986 $allChildVariants = Arr::pluck($variants, 'other_info.bundle_child_ids');
1858 1987
1859 1988 $allChildVariants = array_unique(Arr::flatten($allChildVariants));
1860 - $allChildVariants = Arr::except(
1989 + $allChildVariants = array_values(array_diff(
1861 1990 $allChildVariants,
1862 1991 Arr::pluck($variants, 'id')
1863 - );
1992 + ));
1864 1993 $allChildVariants = array_filter($allChildVariants);
1865 1994 $childVariants = ProductVariation::query()
1866 1995 ->whereIn('id', $allChildVariants)
1867 1996 ->with('product:ID,post_title')