feat(time-tracking): split billable from worked minutes per entry (PMS-395) #384

Merged
Claude-Run merged 1 commit from feat/PMS-395-billable-minutes-split into main 2026-06-26 14:29:07 +02:00
Member

A time entry carried one duration_minutes that was both the worked time and the billing basis, and rounding mutated that single figure to match the billing increment. Billable time can legitimately exceed worked time (minimum increments, one update billed across several clients) or fall below it (internal absorption); the single-field model could not represent this.

Split the concept into two persisted columns on time_entries: worked_minutes (actual time, canonical, never rounded) and billable_minutes (time billed, independent). duration_minutes is retained for one release and tracks worked_minutes.

  • Migration 081 adds both columns and backfills worked = duration, billable = duration when billable else 0.
  • DTOs expose worked_minutes and billable_minutes on create/update requests and the response. Omitting billable_minutes on create defaults it to the rounded worked time for billable entries (pre-change behavior); the worked figure is stored unrounded.
  • create_time_entry / update_time_entry stop rounding the worked figure; rounding now derives only billable minutes, and total_amount is priced on billable minutes. The per-day cap is enforced on worked time.
  • list_timesheets reports total_minutes = SUM(worked) and billable_minutes = SUM(billable), each falling back to duration for legacy rows.
  • The custom report TIME source gains project / contract dimensions and worked-minutes, billable-minutes, and realization (billed / worked) measures so reports can surface over/under-billing per project and contract.

Integration tests cover billable > worked round-tripping and the omitted-billable default.

#PMS-395

A time entry carried one `duration_minutes` that was both the worked time and the billing basis, and rounding mutated that single figure to match the billing increment. Billable time can legitimately exceed worked time (minimum increments, one update billed across several clients) or fall below it (internal absorption); the single-field model could not represent this. Split the concept into two persisted columns on `time_entries`: `worked_minutes` (actual time, canonical, never rounded) and `billable_minutes` (time billed, independent). `duration_minutes` is retained for one release and tracks `worked_minutes`. - Migration 081 adds both columns and backfills worked = duration, billable = duration when billable else 0. - DTOs expose `worked_minutes` and `billable_minutes` on create/update requests and the response. Omitting `billable_minutes` on create defaults it to the rounded worked time for billable entries (pre-change behavior); the worked figure is stored unrounded. - `create_time_entry` / `update_time_entry` stop rounding the worked figure; rounding now derives only billable minutes, and `total_amount` is priced on billable minutes. The per-day cap is enforced on worked time. - `list_timesheets` reports total_minutes = SUM(worked) and billable_minutes = SUM(billable), each falling back to duration for legacy rows. - The custom report TIME source gains project / contract dimensions and worked-minutes, billable-minutes, and realization (billed / worked) measures so reports can surface over/under-billing per project and contract. Integration tests cover billable > worked round-tripping and the omitted-billable default. #PMS-395
feat(time-tracking): split billable from worked minutes per entry (PMS-395)
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 27s
Check / fmt + clippy + build + tests (pull_request) Successful in 1m41s
Integration / integration tests (pull_request) Successful in 7m47s
Create release / Gate (release-branch merges only) (pull_request) Successful in 0s
Create release / Create release from merged PR (pull_request) Has been skipped
0ddb5d855d
A time entry carried one `duration_minutes` that was both the worked time and the billing basis, and rounding mutated that single figure to match the billing increment. Billable time can legitimately exceed worked time (minimum increments, one update billed across several clients) or fall below it (internal absorption); the single-field model could not represent this.

Split the concept into two persisted columns on `time_entries`: `worked_minutes` (actual time, canonical, never rounded) and `billable_minutes` (time billed, independent). `duration_minutes` is retained for one release and tracks `worked_minutes`.

- Migration 081 adds both columns and backfills worked = duration, billable = duration when billable else 0.
- DTOs expose `worked_minutes` and `billable_minutes` on create/update requests and the response. Omitting `billable_minutes` on create defaults it to the rounded worked time for billable entries (pre-change behavior); the worked figure is stored unrounded.
- `create_time_entry` / `update_time_entry` stop rounding the worked figure; rounding now derives only billable minutes, and `total_amount` is priced on billable minutes. The per-day cap is enforced on worked time.
- `list_timesheets` reports total_minutes = SUM(worked) and billable_minutes = SUM(billable), each falling back to duration for legacy rows.
- The custom report TIME source gains project / contract dimensions and worked-minutes, billable-minutes, and realization (billed / worked) measures so reports can surface over/under-billing per project and contract.

Integration tests cover billable > worked round-tripping and the omitted-billable default.

#PMS-395
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!384
No description provided.