- Shipped
- August 27, 2026 at 7:42 AM UTC
- Author
- Kamo
- Commit
- 29ced39
ServiceJob has been a mapped entity with repositories and passing tests and no way to read a row. This gives it the header the finished work-order UI already renders, so the grid can stop generating its own. Shaped on ServiceTaskBookApi/Service: String ids and ISO-8601 strings on the wire, orgId first on every method, every read mapped to a view INSIDE the transaction because ServiceJob.visits is LAZY, and the tenant predicate in the JPQL rather than a check after the load. ServiceJobRepository already carried all four queries, so it is untouched. Two shape decisions the DTO had to make: - totalLabourMinutes is an Integer, never float hours. A 20-minute task is 0.333 hours and three of them are not an hour; ServiceLineItem.labourMinutes argues the case for the column and it holds on the wire too. It is summed from the visits' actual windows, which is the only labour a job can reach: ServiceLineItem hangs off an Order with no link to a job. Time attended, not time billed. - trade is not the UI's category. Ten free composite strings against fifteen ServiceTrade constants, not 1:1 — 'Plumbing Repair' is PLUMBING plus WorkClass.REPAIR, and the work class lives on a line item. The wire carries the trade and the client renders a label. Status moves through ServiceJobStatusMachine and nowhere else, which is why JobInput has no status member and StatusInput is its own record. A status reachable through the general update would be a second door onto the column, and the second door is the one nobody checks. Tests are behavioural, not source scans: the repository is a Proxy whose findByIdForOrg honours the org and whose findById deliberately does not, so an unscoped read is caught as a leak rather than as a break. Mutation-tested — assertTransition deleted, assertTransition moved after setStatus, the org-scoped query swapped for findById, the minutes converted to hours, the status filter moved out of SQL, getVisits() left unread, \@Transactional removed, and a 'status' component added to JobInput. Each produced a distinct failure naming the property it guards.