Core Pricing
A docket brought over from Core keeps the priced lines Core gave it. Its total is the sum of those lines.
What still works
Hours, breaks, activities, workers and the header can all be edited and saved. Submit, review and confirm work as usual.
What does not happen
Pro never recalculates the docket’s price. Not on an edit, a submit, a worker rate change, a bulk edit or a sign-on. To change the money, add or edit a line.
The price preview on a Core docket shows its saved price.
If every Core line on a docket is deleted, it becomes a normal Pro docket and Pro prices it from then on.
Sign-ons
A Core sign-on for a shift that already has a docket for that worker counts as billed. No second docket is made for it.
A sign-on only joins a draft docket. It never changes the rates or price of a docket that has been submitted, reviewed or confirmed.
History
These are recorded in the audit log:
- a worker’s rate change on the field users page
- every sign-on the system attaches to a docket
- every worker row the system gives a classification
For developers
The marker. A docket holds Core pricing while it has a live
docket_pricing_lines row with source_entity_type = 'legacy_core_docket_line'
(deleted_at IS NULL). A line marked removed still counts. Status, the
pricing blob and dockets.metadata play no part.
The guard. The three reprice bodies in backend/pkg/docketpricing return
before the engine runs when the marker is there:
| Body | File | How it asks |
|---|---|---|
| Labour, plant, combined | reprice.go | holds_core_lines on GetDocketForPricing |
| Subcontractor | subcontractor.go | holds_core_lines on GetDocketForPricing |
| Materials | materials.go | DocketHoldsLegacyCoreLines |
The guard returns nil, never an error, and writes nothing: no lines, no
totals, no priced_at, no blob. Every caller (edits, submit, auto-fix, unpin,
the negotiation hook, the Temporal bulk recompute, the sign-on back-fill) goes
through one of these bodies. TestCoreLinePredicateIsOneRule fails if either
SQL copy of the predicate drifts from the other or from the Go constant.
Sign-ons. The individual-docket attach in backend/api/internal/prestart
adopts a found docket only while it is a draft (attachAdoptsFoundDocket). For
any other status it attaches no machine, stamps no rate, does not reprice and
mints nothing. It still records a second supervisor on the docket, which moves
no money.
ListUnbilledSignOnsForWorker leaves out a Core sign-on (metadata.legacy.source = 'core') whose worker already has a docket on that shift, and any sign-on
whose worker already has an individual docket past draft on that shift on a row
that is not prestart_only. A wet-hire (prestart_only) row is left in, because
a plain-labour sign-on skips it and mints the labour docket. A second wet-hire
sign-on on a locked wet-hire docket stays listed and is refused each time; no
money moves.
Preview. PreviewPricing reads the saved docket once, under the caller’s
own docket scope, and reads its lines once. The bills_to freeze uses that same
row’s status. If a line is Core’s, it returns the
saved pricing view, the same one docket detail shows, before any engine work. A
docket outside the caller’s scope is ignored and the preview is the plain engine
run.
Audit actions. worker.attached_from_sign_on and
worker.classification_backfilled on the docket, and field_user_updated on the
project member when the single field-user save changes the rate. The back-fill,
the classification patch and the crew fan-out write their rows in one statement
after their loops (CreateDocketAuditLogsBatch).
Known gap. A Core line in manual mode bills from its cells. Deleting an
activity that line has cells on, then editing the line, recomputes its amount.
The reprice guard does not cover that path.
Tests. backend/pkg/docketpricing/core_pricing_test.go,
backend/api/internal/docket/core_pricing_guard_integration_test.go,
backend/api/internal/prestart/core_signon_backfill_integration_test.go and
backend/api/internal/fielduser/rate_change_audit_integration_test.go.