Skip to main content

Campaign Revenue Report: order list shows wrong/old orders and "Re-Sync Revenue" destroys cached revenue

# [Bug] Campaign Revenue Report: order list shows wrong/old orders and "Re-Sync Revenue" destroys cached revenue β€” `Collection::map('intval')` passes the array key as the `intval()` base

## Environment

- FluentCRM: 3.1.0
- FluentCRM Pro: 3.1.0
- WordPress: 7.0
- WooCommerce: 10.8.1 (tested with both HPOS-authoritative and WP-posts/legacy authoritative; compatibility sync on)
- PHP: 8.3.30
- Database: MariaDB 11.4.10
- Theme: Astra child
- Note: we had to downgrade to FluentCRM 2.9.87 / Pro 2.9.86 to recover; 2.9.x does not have this bug.

## Summary

In `FluentCrm\App\Http\Controllers\CampaignAnalyticsController::getAttributedOrderIds()`, each source branch ends with a point-free `intval` map:

```php
// woo + HPOS branch
->pluck('order_id')->map('intval')->all();

// woo + legacy branch
->pluck('post_id')->map('intval')->all();

```

The wpFluent `Collection::map()` (`vendor/wpfluent/framework/src/WPFluent/Support/Collection.php`) is implemented as:

```php
public function map(callable $callback)
{
Β  Β  $keys = array_keys($this->items);
Β  Β  $items = array_map($callback, $this->items, $keys); // <-- passes (value, key)
Β  Β  return new static(array_combine($keys, $items));
}
```

Because `array_map` receives the keys array as a second source, the callback is invoked as `$callback($value, $key)`. With `'intval'` this becomes `intval($value, $key)`, so the array index is used as the integer base of `intval()`. Only index `0` (base 0 = auto-detect) is parsed correctly; every subsequent order ID is corrupted.

## Reproduction

```php
$ids = ['225345','224377','223655','223194','222906','222262'];
collect($ids)->map('intval')->all();
// Actual: Β  [225345, 0, 0, 8, 42, 312]
// Expected: [225345, 224377, 223655, 223194, 222906, 222262]
```

Why: `intval('224377', 1) === 0` (base 1 invalid), `intval('223655', 2) === 0` (`2` invalid in base 2), `intval('223194', 3) === 8` (`'22'` in base 3), `intval('222906', 4) === 42`, `intval('222262', 5) === 312`.

Real-world: open a campaign Revenue Report (or press Re-Sync Revenue) for a campaign with many attributed orders.

## Impact

  1. Revenue Report order list (`getRevenueReport`): only the newest attributed order renders correctly. The rest become invalid IDs (`0` β†’ skipped) or coincidentally-valid ancient order IDs (e.g. `8`, `42`) that are then shown as unrelated, years-old orders. The `total` counter stays correct (array length), which masks the bug.

  2. "Re-Sync Revenue" (`getRevenueReSyncReport` β†’ `reSyncSourceRevenue`) iterates these corrupted IDs through `wc_get_order()`, sums a near-random subset, and overwrites the cached `_campaign_revenue` meta with a wrong (much smaller) value. Pressing Re-Sync therefore permanently destroys a campaign's historical revenue figure. (In our case a campaign dropped from 2,350,771 to 107,903.)

The underlying `_fc_cid` attribution data is intact in both `postmeta` and `wc_orders_meta`; only the in-memory ID list is corrupted by the map call, so the damage is fully recoverable once the code is fixed.

## Suggested fix

Use an explicit single-argument closure so the key is not passed as a base:

```php
->pluck('post_id')->map(fn($v) => (int) $v)->all();
```

More broadly, consider making `Collection::map()` not forward keys to callbacks that are not key-aware (`intval`, `strval`, `floatval`, etc. are common point-free callables that silently break under a `(value, key)` invocation).

## Secondary observations (same feature; lower priority)

- `getAttributedOrderIds()` (woo + HPOS) reads `wc_orders_meta` exclusively, with no `postmeta` fallback and no sync-completeness guard. During an in-progress HPOS backfill this returns a partial set, compounding the issue above.

- `reSyncSourceRevenue()` switched to paid-net semantics (only `wc_get_is_paid_statuses()`, refunds subtracted) versus the v2.9.x all-status sum. This silently changes historical numbers after upgrade (e.g. cancelled/on-hold orders that used to count no longer do). Worth calling out in the release notes / migration guide.

Thanks!