Files
rippr/docs/v3/V3-10-trip-splitting.md
Dylan 342a8f8382 Break v3 into 16 tickets
One file per feature, in the v2 shape that worked: goal, context, design,
implementation, acceptance criteria, tests, risks, out of scope -- written before
implementing so the risks are on paper rather than walked into.

Three carry more weight than their size suggests. V3-01 ships the port's first
real migration, and since the destructive fallback is gone, getting addColumn and
a v1-database test right matters more than the feature. V3-08 forces a
routing-engine decision with ongoing cost, so it sits behind a RoutingService
interface mirroring what LocationSource did for GPS. V3-13 is not code at all --
it answers the three questions open since v2, and V3-15 may close unbuilt as a
result, which is a legitimate outcome.

Several tickets record constraints that are easy to lose: no activity picker in
front of Start, because the founding premise is press-and-go with gloves; the
Route-to-Trip foreign key must not cascade, or deleting an old plan deletes the
ride; V3-14 will deliberately break the parity harness by adding GPX <type>, and
that expectation should be updated rather than the check dropped; and V3-16 must
not regress the explicit text colours that exist because of the black-on-black bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 10:41:35 -05:00

2.2 KiB

V3-10 — Trip splitting

Phase Ride management · Depends on nothing · Size S · Status Not started

Goal

Split one recorded ride into two at a chosen point. The natural counterpart to merge.

Context

Merge exists and is well tested; split does not. The case is a rider who forgot to stop — one "ride" that is really the trip out, lunch, and the trip home.

Merge already establishes the hard parts: re-parenting points and segments inside a transaction, and recomputing aggregates rather than summing them.

Design

Split at a segment boundary rather than an arbitrary point. Segments already mark where the rider paused, which is exactly where a forgotten stop shows up — and it avoids inventing a new boundary type or splitting a segment in half.

The original trip keeps the earlier segments; a new trip takes the later ones. Both get aggregates recomputed from the points they actually own.

If a ride has only one segment there is nothing to split, and the UI should say so rather than offering a dead control.

Implementation

  1. TripRepository.splitTrip(tripId, atSegmentId) inside a transaction: create the new trip, re-parent segments and points from atSegmentId onward, set startedAt/endedAt from the segments each trip now owns, recompute aggregates for both
  2. Trip detail: a split action listing segment boundaries with their times
  3. Confirmation naming what the two resulting rides will be

Acceptance criteria

  • Splitting produces two trips whose point counts sum to the original
  • Neither trip's distance includes the gap between them
  • Both have plausible startedAt/endedAt
  • Single-segment rides cannot be split, and the UI explains why
  • Atomic — a failure part-way leaves the original intact

Tests

  • Point counts sum; no points orphaned
  • Distance of the parts is less than the original by roughly the gap
  • Split then merge returns to the original aggregates — a good round-trip property
  • Rejects a single-segment trip
  • Atomicity under a forced mid-transaction failure

Risks

Getting startedAt/endedAt from the wrong source. Derive them from the segments each trip owns, not from the original trip.

Out of scope

Splitting mid-segment.