Add carrier management, settlement, and tendering features - #545
Conversation
…nd permissions Introduces the carrier aggregate for brokered freight: carriers with DOT/MC/SCAC identity, compliance status, safety rating, W-9/1099 tax info, and remittance details, plus carrier contacts and insurance policies with expiration tracking. Mirrors the customer aggregate end to end: Bun repository with buncolgen helpers and cursor pagination, tenanted validator with code/DOT/SCAC uniqueness, audit and realtime invalidation, REST handler, GraphQL connection query and select options, permission resource and routes, report catalog entry, TCA allowlist, GTC search pipeline, and Meilisearch index config. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Adds the carrier management UI to the web app: CarrierTable GraphQL operations and generated client, zod schemas mirroring the server-side validation rules, data-table config, /dispatch/carriers route with a five-tab form (identity, compliance and insurance with an inline policy editor, tax, remittance, contacts), carrier autocomplete field, enum choice lists, bulk status service, and navigation entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Lets dispatch cover a shipment move with an external carrier instead of a company driver. Adds carrier_assignments with negotiated buy rates (flat or per-mile plus fuel surcharge and accessorial lines) and external driver/equipment details, a coverage_type discriminator on shipment_moves, and an active-coverage partial unique index. Coverage feeds the shared shipment state coordinator, so carrier-covered moves derive Assigned status through the same machinery as driver assignments. Driver and carrier assignments are mutually exclusive, and carrier eligibility is gated on status, compliance qualification, and required insurance (expired coverage blocks; coverage expiring within 30 days warns with a dispatcher override). Surfaces: REST endpoints under /shipment-moves/:moveID/carrier-assignment, dispatchAssignMoveToCarrier / dispatchCancelCarrierAssignment mutations and an eligibility preview query, carrier coverage columns on the dispatch board with coverage-aware summary counts, and carrierAssignment plus coverageType on the ShipmentMove GraphQL type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Generates a signed-rate agreement PDF for a move's active carrier assignment via the document template system: new rateconfirmation.pdf and rateconfirmation.email template kinds with built-in starters, a rate_confirmations table with per-assignment revisioning (one active revision at a time; regenerating voids the prior one), and a frozen payload snapshot so the sent paper and the payable can never silently diverge. The rendered PDF is filed as a shipment document under a new RATECON system document type (seeded for new organizations and backfilled for existing ones). Sending emails the confirmation to carrier contacts flagged as rate confirmation recipients, with the carrier's email as fallback, and fails soft with a clear error when no email service is configured. Confirming records who accepted and flips the carrier assignment to Confirmed. REST endpoints cover generate, list, get, send, confirm, and void, guarded by a new rate_confirmation permission resource. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Surfaces carrier coverage across the web app. The dispatch console gains an assign-to-carrier dialog with live eligibility preview (blockers disable submit; insurance warnings require an explicit override), carrier badges with total cost on the kanban and inspector, and a cancel-with-reason flow. The shipment panel's assignment dialog gets a Driver | Carrier segmented toggle backed by the REST endpoints, with mutual-exclusion states in both directions, and move cards render carrier coverage details with a rate summary. Command-center cells and the timeline group carrier-covered moves into carrier rows instead of showing them as unassigned. Adds carrier assignment zod schemas, GraphQL operations for the new dispatch mutations and preview query, choice lists, and a carrier assignment REST service; extends timeline data tests to cover carrier rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
…oice matching Builds the purchased-transportation payables spine, mirroring the driver settlement stack. Completed carrier-covered moves accrue cost events (linehaul, fuel surcharge, accessorials) from the assignment's buy rate at the tenant-configured pay trigger, with idempotency keys and void-on-cancel, wired into the same shipment mutation observers the driver accrual uses. Batches generate per period from a new carrier_settlement_controls table, and settlements walk Draft->PendingApproval->Approved->Posted->Paid with a full transition matrix. Posting reuses the shared journal path: debit purchased-transportation expense (per-line GL override, control default, or accounting-control fallback) and credit AP, gated on an open fiscal period, numbered via a new carrier settlement sequence type, and recorded under new CarrierSettlementPosted/Voided/Paid journal source events. MarkPaid posts the AP-relieving payment journal (DR AP / CR cash) and appends to a new carrier ledger subledger (Bill/Payment/Adjustment entries), so the AP balance always reconciles. Remittance CSV export covers batches. Inbound EDI 210 carrier invoices gain a carrier link (SCAC/DOT suggestion plus explicit link) and a matching workflow against the assignment's agreed cost with a configurable variance tolerance; accepting with variance creates an adjustment cost event for the next settlement. Includes an hourly Temporal batch-generation job, GraphQL workspace/lifecycle/matching API, three new permission resources, and golden tests for posting legs, transitions, accrual idempotency, and variance math. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
…d rate confirmation UI Completes the carrier procurement frontend. Adds a Carrier Settlements module with a three-pane settlement workspace (summary strip, multi-select queue with bulk lifecycle actions, detail center, carrier context rail with unsettled cost events and AP subledger balance), settlement and batch and cost event tables, a MarkPaid dialog seeded from the carrier's payment preference, remittance CSV export, and an admin control form with GL account selects and a money-input variance tolerance. The invoice matching route mirrors the invoice reconciliation UX: dual queues over EDI carrier invoices and matches, SCAC/DOT carrier suggestion and explicit linking, and a side-by-side compare of invoice charges against the assignment buy-rate breakdown with variance and tolerance context, supporting accept, accept-with-variance (showing the adjustment it creates), and reject with a note. Rate confirmation actions (generate/send/confirm/void with revision chips and filed-PDF links) surface on shipment move cards and the dispatch inspector's carrier coverage card. Adds status badges, choice lists including the three new journal source events, zod types, and lifecycle helper tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Two correctness-critical fixes from the branch bug sweep. The shipment move repository's explicit column lists omitted coverage_type, so carrier coverage never persisted: boards rendered brokered moves as uncovered, re-brokering was blocked in the UI, and settlement accrual never fired. Both insert and update now write the column, and updates preserve the stored coverage when a payload arrives without one rather than resetting it through the default. Carrier settlement posting previously re-resolved GL accounts from the mutable control tables on every journal, so an AP account change between Post and MarkPaid or Void could credit one account and debit another permanently. Post now stamps the resolved expense and AP accounts onto the settlement, and the paid and void journals build against that snapshot, failing loudly if it is absent. Also adds the sweep migration: FK hardening on accounting rows (shipment delete can no longer cascade away posted cost events; settlement lines, cost events, and accessorials gain proper composite FKs), a one-open-match unique guard for document-AI invoice sources, one-settlement-per-carrier-period uniqueness, the match-to-adjustment linkage column, missing accrual indexes, the base_amount check, and a rename of the outbound rate confirmation document type to distinguish it from the inbound one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
…tching UI Fixes from the branch bug sweep. Carrier replace from the shipment panel no longer fails validation on untouched fields: the assignment payload schema now coerces GraphQL decimal strings to numbers, with a regression test. Rate confirmation actions are scoped to the active carrier assignment, so a replaced carrier's documents can no longer be shown or resent. The settlement lifecycle helper only offers Approve from PendingApproval, matching the server's transition matrix, and the read-only settlement history table drops its bulk lifecycle actions. Timeline drags can no longer drop driver bars onto carrier rows or prefill carrier row keys as worker IDs. The invoice-matching link dialog resets per invoice and picks up the suggested carrier, the plain Accept action is hidden on Variance matches, and a missing settlement-control read hides tolerance framing instead of claiming a zero tolerance. Settlement detail actions now invalidate the full workspace query set so the context rail refreshes. Also: carrier-specific mark-paid methods on bulk pay, compliance and safety-rating badges with real variants, choice-list reuse replacing diverging inline label maps, filter/sort metadata on settlement and batch columns, a console Replace Carrier entry, a shared settlement date formatter replacing nine local copies, and small cleanups. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Closes the remaining sweep findings. Cost events now survive reality: canceling or replacing a carrier assignment voids the stale events and re-accrues the new assignment through a narrow CarrierCostAccrual port, the accrual sweep runs even when no active assignment remains, and a bumped assignment version reprices pending events. Document-AI invoice matches gain the same one-open-match guard EDI invoices have, per-mile assignments are rejected when the move has no distance, and concurrent batch generation degrades to an idempotent skip with attached-event verification. Rate confirmations: sending re-hydrates the frozen snapshot into the typed context so logos survive re-render, generation runs revision resolution and creation in one transaction with a friendly error on races, voiding a confirmed agreement reverts the assignment, and the confirm path is atomic. Assignment cancel and replace void the prior rate confirmation and now emit shipment events and realtime invalidation. Dispatch board coverage counts brokered moves as covered, and settlement adjustment edits are transactional with stable line IDs. DRY extractions mandated by the sweep: a single control-aware delay threshold resolver (fixing the copy that dropped the AutoDelayShipments check), shared shipment clone/find/assignability helpers, a shared US state reference check, and a settlementshared package for status ranks, period resolution, posting workflow, and CSV helpers used by both driver and carrier settlement, with a CR escape fix. Carrier child rows switch to preserve-ID upserts so contact and insurance history stops churning on every save. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
…, EDI channels Introduces the tender domain package (RoutingGuide/RoutingGuideEntry with lane-specificity tiers, Tender/TenderOffer state machines for waterfall and spot tendering, single-use offer tokens), the CarrierEDIChannel carrier child linking carriers to EDI partners, tender shipment event types, the carrier tender offer email template kind with starter assets, routing_guide and tender permission resources, and the carrier_tendering migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Adds RoutingGuideRepository (list/get/create/update/delete with preserve-ID entry upsert, plus tiered MatchLane: exact location pair, then city/state, then state pair), TenderRepository (CAS tender/offer status transitions, sweep listing, workflow-id binding, late-response recording, and single-use token operations), and carrier EDI channel persistence and lookups on the carrier aggregate. Registers both repositories in the FX module. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
…ording routingguideservice provides guide CRUD with save-time validation that EDI-channel entries have a default carrier EDI channel, plus lane matching for dispatch previews. tenderservice creates waterfall tenders (auto-matched or explicit guide) and spot tenders (broadcast or sequential) with full coverage guards, persists the offer plan, and starts the owning Temporal workflow; every carrier response — email token, EDI, or manual — funnels through RecordResponse, which signals the workflow rather than mutating state, and late responses are preserved without status changes. Cancel routes through the workflow with a direct fallback, revokes offer tokens, and is hooked into shipment cancellation and out-of-tender carrier assignment via the new TenderGuard port. carrierassignmentservice gains ConfirmAssignment for the accept flow and implements CarrierMoveAssigner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
CarrierTenderWorkflow owns every tender state transition: the waterfall/ sequential loop dispatches each rank, awaits a response signal or the offer TTL, and advances on decline, expiry, or delivery failure; the broadcast loop dispatches all offers and lets the first acceptance supersede the rest. Responses, cancels, and sweep nudges arrive on one signal channel and are serialized by the workflow, with CAS transitions in the service layer making every activity idempotent and restart-safe. Acceptance lands on the carrier assignment spine (moving the tender terminal first so the coverage guard cannot withdraw it), falls back to NeedsReview with a high-priority notification when auto-assign is blocked, and exhaustion notifies dispatch. A 5-minute sweep nudges overdue workflows and restarts lost ones from the rows. Offer emails render the tender.offer.email template with single-use accept/decline token links under the new tendering.publicBaseUrl config; EDI delivery goes through the EDITenderChannel port. Includes Temporal test-suite coverage for expiry, accept, decline, needs-review, broadcast, cancel, and delivery-failure paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
The EDI channel implements SendTenderOffer: a cost-side 204 built from the tendered move (lane, equipment, weight, offered rate — never customer revenue), carrying the offer id in an L11 reference and the carrier's SCAC (channel override, then carrier master, then partner settings), generated through the existing document pipeline with the org-to-org recipient upsert suppressed and delivery pinned to the carrier channel's communication profile. Inbound 990s route offer-first: a tof_ reference resolves the offer, verifies the sending partner, and funnels accept/decline through the tender workflow; 990s without an offer reference fall through to the legacy org-to-org path unchanged. Public token endpoints serve the emailed links: GET preview never mutates (scanner-safe), POST accept/decline records the response through the same funnel with per-token throttling on top of the global IP limiter, and one vague error covers every invalid-token case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
…er chips Adds read-only GraphQL: routing guide connection listing with projection column selection, single fetch, and lane matching; tender fetch, by-shipment listing, and live-tender-by-move — all bound directly to the tender domain types and gated by the routing_guide and tender read permissions. Offer tokens have no schema surface. The dispatch board move payload gains a liveTender summary (status, mode, rank, current carrier, offer expiry) batch-loaded in one query per board render. REST handlers for guide CRUD and tender lifecycle operations were added alongside, registered with the new permission resources. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
…offer pages Routing guide management at /dispatch/routing-guides: connection-backed table, tiered lane predicate form (exact locations, city/state, or state pair), and a ranked carrier entry editor with rate, expiry, and channel per rank — server field errors land on the offending rows. The dispatch console gains a tender dialog (auto-matched waterfall preview with guide override, spot broadcast/sequential line editor), tender chips with live countdowns on board cards, an inspector coverage card with ranked offer progress, record-response and cancel actions, a NeedsReview banner wired to manual assignment, and a per-shipment tender history section — all tender mutations excluded from the console undo stack. Public accept/decline pages serve the emailed links outside the authenticated app with explicit confirmation, already-responded and invalid-link states. Shared tender and routing guide schemas mirror server validation, tender status badges join the house badge set, and the decimal coercion helper is promoted to shared. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Three-pass adversarial review of the tendering feature; every fix verified at file:line. Acceptance now commits the tender-terminal and offer-accepted transitions in one transaction and is retry-safe end to end: a re-run resumes from the rows (including idempotent assignment: an existing assignment for the accepting carrier is confirmed, not duplicated) instead of misreading its own progress as a lost race, and transient assignment failures retry rather than parking real acceptances in review. Responses that lose the race against expiry while their signal is in flight are now preserved as late responses instead of vanishing. Workflow identifiers are derived deterministically from the tender id everywhere, and the sweep gains a second arm that recovers tenders whose workflow died before its first dispatch or after its last offer resolved. Offer dispatch re-drives delivery on retry (idempotent email key, stamped EDI message) instead of adopting a possibly-unsent offer, separates transient delivery failures (retried) from configuration failures (offer marked failed), guards the missing public-base-URL case, and finally writes current_rank. NeedsReview closes out sibling offers and revokes their tokens, survives the one-live-tender unique index, and reads as a warning in the UI. Offer emails now carry the organization name and equipment summary. Routing guide create/update run in transactions with a rank-shift phase so reordering entries no longer trips the unique rank index, matching states case-insensitively in lane matching; carrier EDI default swaps clear the old default first. The dispatch board degrades to chip-less rendering if tender decoration fails and no longer batches tenders for the driver panel that never renders them. Token crypto is extracted to shared/tokenutils (driver portal migrated), per-mile pricing and rate-method labels collapse onto single domain helpers, the public throttle map gains a hard cap, carrier-supplied decline reasons are length-bounded, and Tender.routingGuide respects routing-guide read permission. Client: live tender views poll so workflow-driven transitions appear, EDI lines drop stale email validation, tender mutation errors surface field-level messages, the public page distinguishes transient failures from dead links with retry, rate formatting and carrier schemas dedupe, and routing guide tables refresh on other users' edits. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
NeedsReview tenders are workflow-stopped but still live for a dispatcher and still hold the move's one-live-tender slot; IsTerminal now agrees with Tender.IsLive and the hand-patched carve-out at its call site is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Swapping partners between two kept channel rows collided mid-statement on the non-deferrable unique index during the single-statement upsert. A DEFERRABLE INITIALLY IMMEDIATE constraint checks at statement end instead; the upsert's ON CONFLICT arbitrates on the primary key, so the deferrable constraint is never an arbiter. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
A move without resolvable stops rendered the subject as "Load offer PRO123: to — $1,500.00"; the lane segment now renders only when both summaries are present. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Inbound 214 statuses and 210 invoices from tendered carriers previously dead-ended: both routes resolve only via edi_tender_recipients, and the carrier tender path deliberately creates no recipient row. A tender-first fallback now fires on recipient miss only: the accepted offer is resolved by the echoed tof_ reference or by partner plus shipment id, pro number, or BOL — anchored on offer status so late documents match after the tender row moves on. A matched 214 posts the same system comment as the legacy path and a shipment event attributed to the carrier; a matched 210 lands the invoice with the carrier and shipment stamped so invoice matching can anchor it to the settlement assignment, with the expected amount set for flat-rate offers. No recipient rows are ever created and shipment tender status is never touched, so the unsolicited-204 and 990 double-apply hazards stay impossible. Separately, a 990 carrying no or an unknown B1-04 reservation code no longer synthesizes a decline: the response is held with a warning while accept stays A and the decline family covers D, N, and R. The legacy org-to-org path is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Single-use tokens for tender offers, shared invoice documents, and portal invitations traveled raw into ginzap access logs (as both message and path/query fields) and the tracing middleware's http.target. A redaction middleware ahead of both now rewrites token path parameters and token query values to a placeholder; handlers are unaffected because path params were extracted at routing time and gin's query cache is primed from the original query string before the rewrite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Neither query had a consumer; both persisted-document safelists shrink by the two orphaned hashes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
EvaluateCarrierEligibility and EligibilityResult are pure functions over carrier data; relocating them from carrierassignmentservice to domain/carrier lets tenderservice screen carriers at tender creation without importing the assignment service (tender -> assignment stays behind ports). Assignment behavior is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Spot tenders now evaluate each line's carrier before offers are built: blockers fail with per-line field errors, and insurance warnings return an overridable business error unless overrideInsuranceWarnings is set — the tender dialog surfaces the warning with a confirm-override resubmit, matching the carrier assignment UX. Waterfall tenders batch-load entry carriers with insurance policies, skip blocked entries with a shipment event per skip plus one dispatch notification, hard-fail when every entry is blocked, and record warn events for passing entries with insurance warnings. GetCarriersByIDs gains filter options so insurance policies load in the same query. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
RecordStopActualRequest gains an optional OccurredAt (nil keeps the current stamp-now behavior). Backdated values are validated against a 5-minute clock-skew ceiling, and ordering guards reject an arrival before any prior stop's departure or a departure before that stop's arrival, since these stamps feed detention and dwell math. Inbound 214 status events report past times and will consume this on the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
X3 records an arrival at the first origin stop, AF a departure from origin, X1 an arrival at the last destination stop, and D1 an arrival if missing followed by a departure at destination; every other status code keeps today's comment-only handling. The 214 event time flows in as the backdated OccurredAt. Comments and shipment events are still always written first, and business or validation failures from the actuals pipeline degrade to routing warnings so a bad stamp never rejects the inbound document — only infrastructure errors propagate. The shipment-move dependencies are optional in the EDI service, and the FX graph confirms no cycle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
New endpoint POST /shipment-moves/:moveID/stops/:stopID/record-actual/ (shipment_move update permission) accepts an arrive or depart action with an optional backdated occurredAt. The shipment move card gains Arrive/Depart actions on stop rows for carrier-covered moves in Assigned or InTransit, with Arrive offered only once prior stops have departed, a confirm dialog offering an optional backdated time (client-side future rejection), and query invalidation refreshing the shipment views from the returned move. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
carrierassignmentservice voids rate confirmations directly through the repository when an assignment is canceled or replaced, bypassing the service-level void that retires outstanding sign links; revoke them at this site too (best-effort — the public resolver independently rejects voided agreements). Also fixes a swagger annotation left pointing at the eligibility type's old package after its move to domain/carrier. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
carrier_settlement_controls gains auto_match_inbound_invoices and auto_accept_within_tolerance (default off), threaded through the domain, repository, GraphQL schema and resolvers, and the settlement control form, where the auto-accept toggle is disabled until auto-match is on. The auto-matcher consuming these flags lands next. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
New CarrierInvoiceAutoMatcher port runs a system-actor flow when a 210 arrives on the accepted-tender path: idempotent via the open-match lookup, it resolves the carrier assignment, creates the invoice match Matched when the variance sits within the settlement tolerance (and resolves it automatically with a nil resolver and an auto note when auto-accept is on) or Variance when outside, feeding the existing review workspace. Match assembly and variance status derivation are extracted from CreateMatch into shared helpers rather than duplicated, and audit entries stamp the system principal since no user acts. The EDI inbound router degrades every matcher failure to file warnings so document routing never fails on matching, and skips invoices lacking a carrier link, references, or a total. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Thirteen shipment event types were emitted by the backend but missing from the GraphQL enum and the client's zod enum: the two carrier assignment types plus the eleven tender types. Because the client's parser throws on unknown values, the shipment activity feed failed outright for any shipment that had ever been tendered or covered by a carrier, and the GraphQL types filter rejected those values. The domain now exports AllTypes and derives IsValid from it, and two parity tests pin the enum in both directions: a Go test asserting the domain and generated GraphQL sets match, and a compile-time client test asserting the zod enum equals the generated union, so future drift fails the build instead of the feed. The activity feed renders all thirteen types explicitly rather than falling back to raw summaries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
The rate confirmation public page has tests and its tender counterpart did not, despite sharing the error-kind helper whose 429/4xx/5xx distinction drives what a carrier sees. Pins the behaviors that matter on an unauthenticated surface: the accept and decline intent URLs pre-open the form but never auto-submit, an invalid token yields the vague state without leaking offer details, an already-answered offer maps to the shared no-longer-available copy, decline requires the confirm click and sends its reason, and throttled or transient failures stay retryable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Every tender notification carried a link to /dispatch-console, which is not a route — the console lives at /dispatch/console — and none of the six event types were registered on the client, so they fell through to a descriptor with no link resolver and the stored link was discarded entirely. Dispatchers got an unclickable grey bell for accepted tenders, exhausted routing guides, failed deliveries and failed rate confirmation issuance. Registers all six with dispatch categories and icons, and teaches the fallback and table-change descriptors to surface a stored link so a producer that bothers to record one always yields a clickable row. Links now deep-link to the covered move via the console's move query param. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Covering a move with a carrier already withdraws any live tender, but assigning a driver did not, so a dispatcher could assign a driver mid-waterfall; the carrier would then accept, the carrier assignment would be rejected on the driver conflict, and the tender would land in NeedsReview — a manual cleanup state reachable through an ordinary dispatch action. Tender creation already refuses to start when a driver assignment exists, so this closes the invariant from the other side. Withdrawal is best-effort and idempotent, mirroring the carrier path, and covers auto-assignment transitively. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Tendering, rate confirmations and carrier assignments had no audit trail at all, while their sibling services audited routinely — leaving "who authorized this carrier at this rate, and when" unanswerable for the brokerage flows where it matters most. Adds entries across tender create/cancel/accept/decline/review, rate confirmation generate/send/confirm/void including the public signing and auto-issued paths, and carrier assign/confirm/cancel. Unattended actors record as the system principal with a comment naming the channel, matching the settlement service's convention. Relatedly, the tender workflow only propagated the acting user for cancel signals, so a dispatcher-recorded accept or decline attributed to the system in both the activity feed and these new entries; the response signals now carry the actor too. Waterfall creation also stops hiding its screening decisions: the create response carries the skipped and warned routing guide entries with their reasons, and the tender dialog shows which carriers were not offered instead of leaving the dispatcher to notice a short offer list. The response stays a superset of the previous body. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
A failed auto-issuance left a move with an accepted tender and a carrier assignment but no rate confirmation — a carrier dispatched against an undocumented rate — and the only signal was a notification. A half-hourly sweep now finds accepted tenders whose covered move has no live rate confirmation and re-runs the idempotent issuer. The query deliberately excludes moves carrying any non-voided confirmation, so a dispatcher-managed revision is never overwritten and the sweep never re-notifies. The two token tables hold hashed credentials for external parties and nothing ever deleted them. A daily job purges tokens thirty days after they were used, revoked or expired; live tokens cannot match the predicate by construction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
GetByWorkflowID was obsoleted by the sweep deriving workflow IDs deterministically and had no callers in production, tests or mocks; two client service methods were likewise unreachable. The table-change allowlist gained the carrier tables during the settlement work but never the tendering ones, so half the domain could not be subscribed to for alerts. The route registry stopped at the carrier pages, leaving the dispatch console and the five carrier settlement pages absent from the computed access manifest — latent today since unregistered routes are absent rather than denied, but a fail-open shape worth closing. The organization-settings control page stays unregistered on purpose: none of its siblings are registered, and adding one alone would make the manifest look more complete than it is. Existing databases need a reset to pick up the new allowlist entries; the seed intentionally no-ops when an org already has rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Seeding left the whole carrier procurement domain empty, so a developer had to hand-build a carrier, then a routing guide, then a shipment before any of it could be exercised. Adds eight carriers chosen to walk the eligibility matrix — clean, expiring inside the warning window, expired policy, missing policy, unqualified compliance, inactive — each with dispatch contacts and remit details, plus a city-pair routing guide whose ranked entries exercise the offered, skipped and warned branches of guide screening, and a state-pair guide demonstrating lane specificity fallthrough. Lanes match the seeded shipments so guides actually match from the console. Tenders and rate confirmations are deliberately not seeded: tender rows are workflow-coupled, and a seeded active tender would have no Temporal workflow for the stalled sweep to find. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
An auto-created invoice match was indistinguishable from one a human made: the row recorded no origin at all. Adds a matchedVia column so the distinction is structural rather than inferred, set to Auto by the inbound auto-matcher and Manual by the dispatcher path, and surfaces it in the matching workspace as a badge plus an All/Auto/Manual filter. Auto-accepted resolutions are detected from the absent resolver rather than by matching the resolution note, which is sound because every manual resolution path requires an actor. The workspace header now also states whether auto-matching and auto-accept are switched on, from the control it already loads. Rate confirmations gain the same treatment: an auto-issued copy is labelled as coming from tender acceptance, and tender history shows whether a carrier answered by email link, EDI, or a dispatcher recording it by hand — provenance the backend has been storing all along without ever showing it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
The tender service already published invalidations on shipment resources, but the realtime map did not route them to the console's tender queries, so acceptance took up to the 15 second poll to appear; the queries themselves documented the gap. Routing them makes the console react immediately, with polling as a floor rather than the only path. Console actions also rendered unconditionally: a user holding only shipment_move read saw Tender to carriers, Cancel tender, Record response and Assign, and learned otherwise from a 403. Each is now gated on the same resource and operation its handler enforces, decided once in the panel rather than per offer row. The live panel also picks up the response-source provenance shown elsewhere in this change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Spec generation had been failing rather than merely going unrun, so the published API docs were missing not just tendering and rate confirmations but carriers, push and the driver portal too — every endpoint written since. Swag resolves qualifiers through a file's own imports when parsing the module twice makes in-module types ambiguous, so four handlers needed blank imports for types their annotations reference. The spec grows from 248 to 307 paths, and a docs-generate-check task keeps it from rotting silently again. Adds a tendering runbook covering the lifecycle and its two sweeps, what NeedsReview means and how a dispatcher clears it, the manual recovery path when rate confirmation issuance fails and what the self-healing sweep will and will not touch, and the token lifecycle. The public web hardening guide picks up the two carrier token surfaces it never covered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
client-preset emits the Partial overload of getFragmentData first, so it won resolution for every call and degraded ordinary fragment data to Partial — which is why regenerating the client broke typecheck across the reports and home-layout modules, and why the committed output had drifted from what codegen produces. Every change since has had to revert this one file by hand, leaving codegen:check red and the generated client stale. A codegen hook now moves that overload after the precise ones, so normal calls resolve exactly while a genuinely partial fragment ref still falls through to it. The script fails loudly if the preset's output changes shape rather than silently leaving the overloads misordered. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Thirteen tags had no catalog entry and fell into an Other bucket: the five this branch added for carriers, tenders, assignments, rate confirmations and routing guides, plus eight that had been stranded there already because the spec was regenerated rarely enough for it not to show. Every tag used by a route now has a group, and the Other bucket is gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
The gqlgen, seed ID and OpenAPI check tasks existed but nothing invoked them — they were referenced only from engineering docs — which is how the API spec came to be stale enough that generation had been failing outright without anyone noticing. A codegen-checks job now runs all three on push and pull request, alongside the client codegen gate that was already wired. The seed check also gains teeth: it swallowed generator errors and reported drift on stdout without failing, so it could never have caught anything. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Adopts master's SQLite support, transport modes, permits and jurisdiction rules, and adapts this branch's work to the gates that came with them. Five migrations here shared version numbers with master's — bun keys migrations by version alone, so of each colliding pair only one would ever have run. Ours move to sub-day offsets, keeping their order after master's same-day migrations and before our later ones. The eleven migrations on this branch also gain the SQLite counterparts the new parallel migration tree requires, and one settlement query trades a hardcoded Postgres epoch expression for the dialect-aware helper. Master fixed the getFragmentData overload problem independently, and better: a hand-written module re-declares the narrowed overloads while the generated file stays byte-identical to codegen. Our codegen hook for the same problem is removed in favour of it. Two fixes carried in from master's side, both breaking its own gates: the jurisdiction rule columns imported ColumnDef from tanstack rather than the shared wrapper that binds the table features, and API_BASE_URL was hardcoded to localhost with its resolver commented out, which fails two document service tests and leaves the resolver unused. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ❌ Deployment failed View logs |
trenova | 0a3b1bf | Aug 12 2026, 09:04 PM |
|
Important Review skippedToo many files! This PR contains 490 files, which is 390 over the limit of 100. To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch. Upgrade to a paid plan to raise the limit. Usage-priced reviews support at most 300 files. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (7)
📒 Files selected for processing (490)
You can disable this status message by setting the Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 35944090 | Triggered | Generic High Entropy Secret | a1e9709 | services/tms/internal/api/middleware/tokenredaction_test.go | View secret |
| 35944091 | Triggered | Generic High Entropy Secret | a1e9709 | services/tms/internal/api/middleware/tokenredaction_test.go | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
1 similar comment
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 35944090 | Triggered | Generic High Entropy Secret | a1e9709 | services/tms/internal/api/middleware/tokenredaction_test.go | View secret |
| 35944091 | Triggered | Generic High Entropy Secret | a1e9709 | services/tms/internal/api/middleware/tokenredaction_test.go | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
The token redaction tests used random-looking strings as fixtures, which GitGuardian flagged as high-entropy secrets. They were never credentials — the middleware redacts by position, not by value, so the tests work identically with the obviously-fake patterned IDs the rest of the suite already uses. The merge commit was made without regenerating, so gqlgen wanted to drop a block of its own parked dead code and the OpenAPI spec was missing every endpoint master added since it was last built. Both regenerated. An unused nolint directive on the SQLite driver's Begin also tripped nolintlint. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG
Description
This PR introduces comprehensive carrier management, settlement processing, and tendering capabilities to the TMS platform. It adds new domain models, GraphQL APIs, UI workflows, and supporting infrastructure for managing carrier relationships, financial settlements, and shipment tendering.
Related Issue or Discussion
This is a major feature addition implementing carrier-centric workflows as part of the platform's expansion.
Type of Change
Scope
Backend (services/tms)
Frontend (client)
/carrier,/carrier-settlement,/carrier-settlement-workspace,/carrier-invoice-matching,/carrier-cost-event,/routing-guide,/dispatch-console(tender features), public pages for rate confirmations and tender offersShared
Configuration & Navigation
Validation
cd services/tms && task test— New domain and service tests addedcd services/tms && task lint— Code follows Go conventionscd client && pnpm build— Client builds successfullycd client && pnpm lint— Client linting passesDeployment Notes
Database Migrations
New tables required for:
carriers— Carrier master data with compliance trackingcarrier_contacts— Carrier contact informationcarrier_insurance_policies— Insurance policy trackingcarrier_edi_channels— EDI configurationcarrier_settlements— Settlement batches and individual settlementscarrier_settlement_adjustments— Manual adjustments to settlementscarrier_invoice_matches— Invoice matching and reconciliationcarrier_cost_events— Cost accrual eventscarrier_ledger_entries— Ledger for settlement accountingrate_confirmations— Rate confirmation records with token-based public accesstenders—https://claude.ai/code/session_01GGqQ3vR9PCHeooQwA3m9NG