An org-scoped read/write API for service jobs

Featurekamo-shared-library
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.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing