Skip to content

feat(time): the billing time entry is a satellite, not a third timeEntry - #1805

Merged
rubenvdlinde merged 1 commit into
developmentfrom
feat/billing-time-entry-satellite
Sep 5, 2026
Merged

feat(time): the billing time entry is a satellite, not a third timeEntry#1805
rubenvdlinde merged 1 commit into
developmentfrom
feat/billing-time-entry-satellite

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Why

Three apps declared a timeEntry. A schema slug is global per organisation and SchemaMapper::find() matches LOWER(slug), so whichever row was reached first answered for all three.

what it is required
humaniq TimeEntry a clocked interval, or since ConductionNL/humaniq#323 a day booking clock times, or date
planninq timeEntry a duration booked to a project task task, user, duration, date
this app's the billing record of time worked title, hours, date

humaniq is the agreed owner of the hours, and this app's record is not a second copy of them. It is client, lead, billing category, approval state, WIP sync status and invoice batch — the billing side of a booking. So it is named for what it is.

What changes

timeEntrybillingTimeEntry, carrying a timeEntry property naming the humaniq booking it bills.

That reference is a plain uuid, not a $ref — humaniq's register is a different register, and ADR-062 rule 7 gives a cross-register target no $ref.

The app-config key timeEntry_schema deliberately does not move. It is live persisted state, and the same split already applies to klantLoyaltyAccount_schema two lines below it in the same map.

RenameTimeEntrySchemaSlug renames the row before the import, modelled on RenameLoyaltyAccountSchemaSlug which exists for exactly this. Without it the descriptor change renames nothing: OpenRegister matches by (application, slug) and its not-found branch creates a second schema, orphaning every existing billing line silently while the app reads an empty collection.

The seedData references move with the schema — seven of them, across both fragments and the mock register. A rename that reaches the schema and not its seed data imports rows against a slug nothing declares, and missing one is invisible until an import runs.

What this does not do yet, deliberately

hours, date, user and description stay here rather than moving onto the humaniq booking, because BillingCategoryWidget sums hours per billingCategory and moving them would make that widget read across two apps.

The fleet already has the pattern: a requiredApp widget, which gate-55 supports and which hides rather than rendering empty when the backing app is absent — and which this app already uses to read planninq's project. Applying it here is the next change, and it is what makes the hours live once.

The collision is cleared either way, which is what this change is for.

Testing

  • 2,855 tests green, 4 new: rename, no-op, both-slugs-refused, duplicate-refused.
  • phpcs, phpmd, psalm and phpstan clean via composer check:strict.
  • All 75 applicable hydra gates pass.

🤖 Generated with Claude Code

Three apps declared a `timeEntry`. A schema slug is global per organisation and
`SchemaMapper::find()` matches `LOWER(slug)`, so whichever row was reached first
answered for all three: humaniq's HR booking, planninq's project booking, and
this app's billing/WIP record.

humaniq is the agreed owner of the hours, and this app's record is not a second
copy of them. It is client, lead, billing category, approval state, WIP sync
status and invoice batch — the BILLING side of a booking. So it is named for
what it is: `billingTimeEntry`, carrying a `timeEntry` property that names the
humaniq booking it bills.

That reference is a plain uuid and not a `$ref`. humaniq's register is a
different register, and ADR-062 rule 7 gives a cross-register target no `$ref`.

The app-config key `timeEntry_schema` deliberately does NOT move — it is live
persisted state, and the same split already applies to
`klantLoyaltyAccount_schema` two lines below it in the same map.

`RenameTimeEntrySchemaSlug` renames the row before the import, modelled on
`RenameLoyaltyAccountSchemaSlug`, which exists for exactly this. Without it the
descriptor change renames nothing: OpenRegister matches by (application, slug)
and its not-found branch CREATES a second schema, orphaning every existing
billing line silently while the app reads an empty collection.

The seedData references move with the schema, in both fragments and the mock
register. A rename that reaches the schema and not its seed data imports rows
against a slug nothing declares — seven references, and missing one is invisible
until an import runs.

WHAT THIS DOES NOT DO YET, and it is deliberate. `hours`, `date`, `user` and
`description` stay here rather than moving onto the humaniq booking, because
`BillingCategoryWidget` sums `hours` per `billingCategory` and moving them would
make that widget read across two apps. The fleet already has the pattern — a
`requiredApp` widget, which gate-55 supports and which HIDES rather than
rendering empty when the backing app is absent, and which this app already uses
to read planninq's `project`. Applying it here is the next change, and it is
what makes the hours live once.

The collision is cleared either way, which is what this change is for.

2,855 tests green (4 new). phpcs, phpmd, psalm and phpstan clean, and all 75
applicable hydra gates pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rubenvdlinde
rubenvdlinde merged commit d86420f into development Sep 5, 2026
34 of 35 checks passed
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/pipelinq @ 67feaaf

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-manifest
check-vue-demi
test-l10n
format
check-schema-l10n
check-l10n-js
composer ✅ 106/106
npm ✅ 637/637
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-05 12:57 UTC

Download the full PDF report from the workflow artifacts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant