Problem/Motivation

Any site with more than one node of the same daily-bookable content type.

When a site manager updates availability for a daily-bookable node, BEE's
`createDailyEvent()` method queries all BAT units belonging to the content
type's UnitType — not just the units belonging to the node being edited. It
then writes availability events for all of them. On a site with multiple nodes
sharing the same bookable content type, editing one node silently overwrites
the availability of every other node of that type.

The hourly equivalent, `createHourlyEvent()`, does not have this bug — it
correctly intersects the type-wide unit list with the units belonging to the
current node. `createDailyEvent()` needs the same treatment.

Cause

`createDailyEvent()` fetches units that currently have a known availability
state via `bat_event_get_matching_units()` (scoped to the content type's
`$type_id`, not the node). It then falls back to the node's own `$units_ids`
if that returns empty. But when results *are* returned (e.g., after the first
event has been written anywhere in the type), the available-unit set is
type-wide, not node-scoped:

// Current (buggy) code:
if (empty($d['available_units'])) {
    $d['available_units'] = $d['units_ids'];   // node-scoped fallback
}

if (!empty($d['available_units'])) {
    // ...
    foreach ($d['available_units'] as $unit) { // may include other nodes' units
        // ...
        $event->set('event_bat_unit_reference', $unit);
        $event->save();
    }
}

`$d['units_ids']` is correctly populated from the current node's
`field_availability_daily` field references. But `$d['available_units']` —
returned by `bat_event_get_matching_units()` — may contain units from other
nodes of the same type, and there is no intersection step to filter it down to
this node's units only.

### Comparison: createHourlyEvent() is already correct

`createHourlyEvent()` was updated (as part of the fix for issue #3576346) to
use `array_intersect()` to limit writes to the current node's own units:

$units = empty($d['available_units'])
    ? $d['units_ids']
    : array_intersect($d['units_ids'], $d['available_units']);

`createDailyEvent()` needs identical treatment.

Impact

- On sites with multiple nodes of the same daily-bookable content type, editing
any node's availability corrupts every other node's calendar state.
- This is a **silent data corruption** — no error is shown; the wrong units
simply receive unexpected availability events.
- Single-node sites are not affected by the scope-leak aspect, but they are
affected by the related BAT root cause (invisible default-state units).

Steps to reproduce

1. Install BEE, configure a "Generally available" **daily** bookable content
type.
2. Create **two** nodes of that type (Node A and Node B). Each gets its own BAT
unit.
3. On Node A, go to **Update Availability** and mark a date range as
"Unavailable."
4. Check Node B's calendar or attempt a booking on Node B for the same date
range.

**Expected:** Node B's availability is unchanged — the edit applied only to
Node A.

**Actual:** Node B is also marked "Unavailable" for that range. BAT calendar
events were written for Node B's unit even though it was not being edited.

Proposed resolution

Replace the two-step `if (empty) → fallback` / `if (!empty) → foreach` pattern
in `createDailyEvent()` with the same `array_intersect` approach used in
`createHourlyEvent()`:

**Before:**

if (empty($d['available_units'])) {
    $d['available_units'] = $d['units_ids'];
}

if (!empty($d['available_units'])) {
    if ($d['new_state'] == 'available') {
        $d['state'] = bat_event_load_state_by_machine_name('bee_daily_available');
    }
    else {
        $d['state'] = bat_event_load_state_by_machine_name('bee_daily_not_available');
    }

    foreach ($d['available_units'] as $unit) {

        $event = bat_event_create(['type' => 'availability_daily']);
        $event_dates = [
            'value' => $d['start_date']->format('Y-m-d\TH:i:00'),
            'end_value' => $end_date->format('Y-m-d\TH:i:00'),
        ];
        $event->set('event_dates', $event_dates);
        $event->set('event_state_reference', $d['state']->id());
        $event->set('event_bat_unit_reference', $unit);
        $event->save();

    }
}

if (isset($d['booked_units'])) {
    foreach ($d['booked_units'] as $unit) {
        $bat_unit = bat_unit_load($unit);
        $this->messenger()->addWarning(...);
    }
}

**After:**

$units = empty($d['available_units'])
    ? $d['units_ids']
    : array_intersect($d['units_ids'], $d['available_units']);

if (!empty($units)) {
    if ($d['new_state'] == 'available') {
        $d['state'] = bat_event_load_state_by_machine_name('bee_daily_available');
    }
    else {
        $d['state'] = bat_event_load_state_by_machine_name('bee_daily_not_available');
    }

    foreach ($units as $unit) {
        $event = bat_event_create(['type' => 'availability_daily']);
        $event_dates = [
            'value' => $d['start_date']->format('Y-m-d\TH:i:00'),
            'end_value' => $end_date->format('Y-m-d\TH:i:00'),
        ];
        $event->set('event_dates', $event_dates);
        $event->set('event_state_reference', $d['state']->id());
        $event->set('event_bat_unit_reference', $unit);
        $event->save();
    }
}

foreach ($d['booked_units'] as $unit) {
    $bat_unit = bat_unit_load($unit);
    $this->messenger()->addWarning(...);
}

The `array_intersect($d['units_ids'], $d['available_units'])` step guarantees
that only units belonging to the node currently being edited are written —
exactly as `createHourlyEvent()` already does.

The `isset()` guard around the booked-units warning loop is also removed, since
`bat_event_get_matching_units()` always returns an array (empty or not), making
the guard unnecessary (again, consistent with `createHourlyEvent()`).

## Relationship to Issue #3576346

Issue #3576346 ("fresh install — no availability events can be written")
introduced the `array_intersect` fix to `createHourlyEvent()` but did not
apply the equivalent fix to `createDailyEvent()`. This issue is the daily-mode
counterpart of that fix, plus the additional scope-leak problem described above.

There is also a deeper root cause in BAT itself (units with no DB rows being
invisible to `bat_event_get_matching_units()`); a separate issue has been filed
against the BAT module for that.

Remaining tasks

I have verified and tested on my local system under DDEV, additional testing by maintainers would be welcome.

User interface changes

None

API changes

none

Data model changes

none

Sponsorship

none

Comments

owens-d created an issue. See original summary.

afagioli’s picture

Issue summary: View changes

improve readability

afagioli’s picture

Hi,

Thank you so much for this incredibly detailed analysis! This is exactly the kind of thorough debugging that makes the Drupal community great. You've clearly put a lot of work into identifying the root cause, comparing it with createHourlyEvent(), and even providing a clear before/after code example.

I really appreciate the effort you've put into this.

One small request to help get this committed faster:

Instead of pasting code directly in the issue queue, please follow Drupal best practices:

* Create an issue fork and push your changes there, then open a Merge Request, or
* Create a patch using git diff and attach it to this issue

Pasting code in the comments makes it harder for maintainers to test, review, and apply your fix. A proper patch or merge request will also ensure you get full credit for your work in the git history.

You've done the hard part already — this last step will make sure your fix actually gets merged.

Thank you again for contributing to make BEE better for everyone!

afagioli’s picture

Status: Active » Needs work