feat(mileage): mileage entries, per-mile rate card rate, invoice line type #277

Merged
nrupard merged 2 commits from feat/pms-315-mileage-entries into main 2026-06-16 21:08:54 +02:00
Owner

Add a first-class mileage entry that mirrors a time entry's FK shape and billing/approval lifecycle but swaps the hours axis for a distance axis (PMS-315 Phase 1, server side).

Migration 051 creates mileage_entries (tenant-scoped, fail-closed RLS + FORCE + updated_at trigger attached explicitly since the 024/038 loops already ran), adds rate_cards.default_per_mile_rate, and widens the invoice_lines.line_type CHECK to admit mileage.

New mokosh-types::mileage_tracking DTOs (CreateMileageEntryRequest / UpdateMileageEntryRequest / MileageEntryResponse / MileageEntryFilter) reuse the time-tracking ApprovalStatus and tickets::BillingStatus. New modules::mileage_tracking exposes CRUD under /api/v1/mileage-entries, gated by the time-tracking module and tenant-scoped end to end; a billable entry inherits the tenant default rate card's per-mile rate when no explicit rate is given and prices total_amount = distance_miles * rate_per_mile. Mileage has no timesheet gate, so a billable entry is ready_to_bill on creation.

The invoice builder (create_invoice_from_time_entries) now sweeps unbilled mileage entries alongside time entries: one mileage line per entry, description Mileage: <start> -> <end> (or bare Mileage when both addresses are empty), and both flip to billed in the same transaction. InvoiceLineType::Mileage added.

Frontend ACs (Log Time Hours/Mileage toggle, list badge, Rate Cards UI) live in the separate mokosh-clients repo and are out of scope for this repo; Phase 2 address-to-address routing is an optional follow-up.

Tested: new tests/mileage_tracking.rs (CRUD, rate inheritance, combined time+mileage invoice, cross-tenant isolation); full integration suite green.

#PMS-315

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Add a first-class mileage entry that mirrors a time entry's FK shape and billing/approval lifecycle but swaps the hours axis for a distance axis (PMS-315 Phase 1, server side). Migration 051 creates `mileage_entries` (tenant-scoped, fail-closed RLS + FORCE + updated_at trigger attached explicitly since the 024/038 loops already ran), adds `rate_cards.default_per_mile_rate`, and widens the `invoice_lines.line_type` CHECK to admit `mileage`. New `mokosh-types::mileage_tracking` DTOs (CreateMileageEntryRequest / UpdateMileageEntryRequest / MileageEntryResponse / MileageEntryFilter) reuse the time-tracking `ApprovalStatus` and `tickets::BillingStatus`. New `modules::mileage_tracking` exposes CRUD under `/api/v1/mileage-entries`, gated by the time-tracking module and tenant-scoped end to end; a billable entry inherits the tenant default rate card's per-mile rate when no explicit rate is given and prices `total_amount = distance_miles * rate_per_mile`. Mileage has no timesheet gate, so a billable entry is `ready_to_bill` on creation. The invoice builder (`create_invoice_from_time_entries`) now sweeps unbilled mileage entries alongside time entries: one `mileage` line per entry, description `Mileage: <start> -> <end>` (or bare `Mileage` when both addresses are empty), and both flip to `billed` in the same transaction. `InvoiceLineType::Mileage` added. Frontend ACs (Log Time Hours/Mileage toggle, list badge, Rate Cards UI) live in the separate mokosh-clients repo and are out of scope for this repo; Phase 2 address-to-address routing is an optional follow-up. Tested: new tests/mileage_tracking.rs (CRUD, rate inheritance, combined time+mileage invoice, cross-tenant isolation); full integration suite green. #PMS-315 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat(mileage): mileage entries, per-mile rate card rate, invoice line type
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 36s
Check / fmt + clippy + compile + unit/doc tests (pull_request) Successful in 57s
Integration / integration tests (pull_request) Successful in 3m2s
ee415e87de
Add a first-class mileage entry that mirrors a time entry's FK shape and billing/approval lifecycle but swaps the hours axis for a distance axis (PMS-315 Phase 1, server side).

Migration 051 creates `mileage_entries` (tenant-scoped, fail-closed RLS + FORCE + updated_at trigger attached explicitly since the 024/038 loops already ran), adds `rate_cards.default_per_mile_rate`, and widens the `invoice_lines.line_type` CHECK to admit `mileage`.

New `mokosh-types::mileage_tracking` DTOs (CreateMileageEntryRequest / UpdateMileageEntryRequest / MileageEntryResponse / MileageEntryFilter) reuse the time-tracking `ApprovalStatus` and `tickets::BillingStatus`. New `modules::mileage_tracking` exposes CRUD under `/api/v1/mileage-entries`, gated by the time-tracking module and tenant-scoped end to end; a billable entry inherits the tenant default rate card's per-mile rate when no explicit rate is given and prices `total_amount = distance_miles * rate_per_mile`. Mileage has no timesheet gate, so a billable entry is `ready_to_bill` on creation.

The invoice builder (`create_invoice_from_time_entries`) now sweeps unbilled mileage entries alongside time entries: one `mileage` line per entry, description `Mileage: <start> -> <end>` (or bare `Mileage` when both addresses are empty), and both flip to `billed` in the same transaction. `InvoiceLineType::Mileage` added.

Frontend ACs (Log Time Hours/Mileage toggle, list badge, Rate Cards UI) live in the separate mokosh-clients repo and are out of scope for this repo; Phase 2 address-to-address routing is an optional follow-up.

Tested: new tests/mileage_tracking.rs (CRUD, rate inheritance, combined time+mileage invoice, cross-tenant isolation); full integration suite green.

#PMS-315

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(mileage,time): validate re-associated ticket_id on update against tenant
All checks were successful
E2E / Playwright against staging (pull_request) Successful in 47s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / fmt + clippy + compile + unit/doc tests (pull_request) Successful in 4m29s
Integration / integration tests (pull_request) Successful in 6m31s
0cd6088dac
The update paths for both mileage_entries and time_entries wrote ticket_id straight from the request body, and the FK only checks existence, not tenant ownership, so a re-association could point an entry at another tenant's ticket. The fail-closed RLS on the tickets table hides the row on read-back (so it is not a data leak), but the link should never be written in the first place. Mirror the create-path guard: validate a Some(ticket_id) via assert_ticket_in_tenant before the UPDATE and return 404 when it is not the tenant's. This closes the create/update asymmetry surfaced in the PMS-315 review and applies the same fix to the pre-existing time_entries gap. Adds UpdateMileageEntryRequest: Default so the new regression test can build a partial update, plus a test asserting a foreign ticket_id is rejected and the stored row keeps no ticket link.

#PMS-315
nrupard deleted branch feat/pms-315-mileage-entries 2026-06-16 21:08:54 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
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!277
No description provided.