Skip to content

fix: worklog filter period to mutation - #350

Merged
tuj merged 3 commits into
developfrom
feature/worklog-filter-period-to-mutation
Sep 8, 2026
Merged

fix: worklog filter period to mutation#350
tuj merged 3 commits into
developfrom
feature/worklog-filter-period-to-mutation

Conversation

@tuj

@tuj tuj commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Link to ticket

https://leantime.itkdev.dk/#/tickets/showTicket/8000

Description

On the invoice-entry worklog picker, the total hours could cover one day more than the
list shown beneath it
whenever a "Periode til" was set.

WorklogRepository::createFilterDataQueryBuilder() did this:

$periodTo = $filterData->periodTo->modify('tomorrow');

\DateTime::modify() mutates the receiver and returns it, so the assignment reads as if
it produced a new value but actually shifted the caller's filter object.
InvoiceEntryWorklogsFilterData::$periodTo is a mutable \DateTime, and
InvoiceEntryWorklogController::worklogs() builds the query twice against the same
filter instance:

$worklogs = $worklogRepository->findByFilterData($project, $invoiceEntry, $filterData);                   // :92
$totalTimeSpentSeconds = $worklogRepository->sumSelectableTimeSpentSecondsByFilterData(...$filterData);   // :93

The first call moves periodTo to D+1 and queries started < D+1 — correct, the whole of
day D. The second call then modifies the already shifted value to D+2 and queries
started < D+2, so the total silently picks up day D+1. The user sees a total that
includes worklogs the list doesn't show, with no way to tell where the extra hours came
from.

The fix is one word — clone before modifying:

$periodTo = (clone $filterData->periodTo)->modify('tomorrow');
Tests

Both were written first and confirmed red against the old code:

  • testFindByFilterDataDoesNotMutatePeriodTo — the root cause. Deterministic, no fixture
    dependency: the repository must not mutate the filter it was handed.
  • testSumSelectableTimeSpentSecondsCoversTheSameDaysAsTheListedWorklogs — the symptom.
    Failed with 10800 is not identical to 3600, i.e. the total carried an extra hour-and-a-
    half worklog the list omitted.

The symptom test persists its own project and two worklogs on consecutive days, via a new
persistProjectWithWorklogs() helper. That's necessary rather than incidental: AppFixtures
derives started from k % 12 + 1 / k % 28 + 1, which spaces each month's days four
apart, so no two fixture worklogs land on consecutive days and a shifted boundary has
nothing to pick up. The helper registers its rows for the existing tearDown() cleanup,
since nothing wraps these tests in a transaction.

Not in scope

createFilterDataQueryBuilder() has a second latent bug that this PR deliberately leaves
alone. Both there (:166-172) and in sumSelectableTimeSpentSecondsByFilterData()
(:147), the "not billed" branch passes a bare string to andWhere():

'worklog.isBilled = FALSE OR worklog.isBilled is NULL'

DQL binds AND tighter than OR, so with any other predicate present this parses as
(project = :project AND … AND isBilled = FALSE) OR isBilled IS NULL — the right-hand side
is unqualified and matches null-billed worklogs from any project. It needs
$qb->expr()->orX(...). It can't be demonstrated against the current fixtures (every
fixture worklog has is_billed set), so whether it bites depends on whether production has
null rows: SELECT COUNT(*) FROM worklog WHERE is_billed IS NULL;. Worth its own branch.

Checklist

  • My code is covered by test cases.
  • My code passes our test (all our tests).
  • My code passes our static analysis suite.
  • My code passes our continuous integration process.

@tuj tuj self-assigned this Aug 27, 2026
@tuj tuj added the bug Something isn't working label Aug 27, 2026
@tuj
tuj requested a review from jeppekroghitk September 2, 2026 18:19
@tuj
tuj merged commit 8eb67f7 into develop Sep 8, 2026
6 checks passed
@tuj
tuj deleted the feature/worklog-filter-period-to-mutation branch September 8, 2026 09:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants