Documentation navigation

Wayfarer

Four-way Region deletion

Issue #848 adds an explicit preview/confirmed operation. The Region and its own name, notes, cover and center are always deleted. Places and Areas each independently select keep or delete. Vue defaults to Keep/Keep. Empty Regions still require confirmation.

Places Areas Result
keep keep Append both to canonical Unassigned Places
keep delete Append Places; delete Areas
delete keep Delete Places; append Areas
delete delete Delete both child sets

Kept children retain their IDs and material contents. Kept Areas retain polygon holes and SRID. Kept Places retain visit associations and all Segment aggregates, route geometry, measurements and provenance. Deleting Places uses canonical endpoint-Segment deletion and surviving waypoint reconciliation; retained visit snapshots become unlinked. Keeping Areas does not preserve deleted Places/routes.

HTTP contract

Both authorities use the same JSON DTOs and lifecycle implementation:

Authority Preview POST Confirmed POST
Editor cookie with User role /api/trips/{tripId}/editor/regions/{regionId}/deletion-preview /api/trips/{tripId}/editor/regions/{regionId}/deletion
Active incoming bearer token, no cookie fallback /api/trips/{tripId}/regions/{regionId}/deletion-preview /api/trips/{tripId}/regions/{regionId}/deletion

Both editor POSTs require the RequestVerificationToken antiforgery header. Ownership is checked before parsing; responses use Cache-Control: no-store. Preview accepts exactly { "places": "keep", "areas": "keep" }. Both strings are required; extra fields, duplicates, numeric values and unsupported choices fail. Confirmed POST additionally accepts confirmationToken from the matching preview. No client destination, counts, child IDs or authored contents are accepted.

Preview returns contract version 1, Trip/source identity and name, both choices, destination: { mode, regionId, name } (existing, create, not-required), regionMetadataLoss: { hasNotes, hasCover, hasCenter }, and confirmation: { token, expiresAt } (ten minutes). consequences contains movedPlaces, deletedPlaces, movedAreas, deletedAreas, deletedSegments, updatedSegments, detachedWaypoints. Each group has complete count, at most five deterministic sampled ids, and hasMore; waypoint IDs are { segmentId, placeId }. Endpoint deletions and waypoint-only changes never overlap. Preview inserts nothing and calls no provider or SaveChanges.

Success on both adapters is EditorMutationResult<RegionDeletionResultDto>: { success, data, affected, deletedIds, warnings }. Data contains tripId, sourceRegionId, both choices, destinationRegionId, complete movedPlaceIds, movedAreaIds, updatedSegmentIds, detachedWaypointAssociations. Deleted IDs are complete. Authoritative slices include changed Region/child/order projections, fresh surviving Segment aggregate tokens, metadata/timestamp and visit progress. Slices are projected before commit; success is returned only after commit.

Status ProblemDetails code Recovery
400 region-deletion-request-invalid Fix fields or obtain an untampered confirmation
401/403/404 Authority/role/protected source/ownership boundary Authenticate or select an owned normal Region
409 region-deletion-confirmation-required Review supplied fresh preview and explicitly confirm
409 region-deletion-preview-stale Obtain fresh preview and explicitly confirm again
409 unassigned_region_conflict Reconcile malformed affected ownership/canonical state

Cookie antiforgery rejection retains framework behavior. Definite pre-commit rejections make no domain change. Participating unconditional Web/bearer DELETE adapters reject with 409 mutation-confirmation-required; the canonical Region remains forbidden (403). Identity-only headers cannot authorize deletion. Use these explicit POSTs; never fall back to DELETE.

Transaction, confirmation and recovery

Preview uses repeatable-read isolation. Confirmation protects a deterministic hash of source metadata, complete authored Place/Area contents, geometry bytes/SRID, Segment scalar contents/provenance, waypoint positions/vertex indices, anchor coordinates, relevant profile inputs, canonical identity or proposed identity plus verified absence, applicable destination membership/orders and ordering siblings. It binds account, Trip, source and both choices; it is not a receipt.

Mutation discovers candidates and holds deterministic row locks through commit: profiles → Segments → Places → Areas → Regions → Trip, IDs sorted within classes. Region/Trip and Segment/Place FK parent barriers cover membership insertion, including legacy writers. Final persisted rediscovery after waits must match the approval and need no additional earlier-class locks. The locked active account and incoming bearer credential are checked again before writes, consistently with protected ordering/move writers. Changes conflict without retry.

Only the exact name Unassigned Places elects a canonical Region, independent of historical order. Wrong ownership fails closed. Missing destinations are created inside the transaction only when at least one child is actually kept. A concurrent winner requires another preview. Places sort by order/null-last/ID; Areas use order/null-last/database-collated name/ID. Append then normalize only kept child types; normalize Segment order only when endpoint deletion occurs.

Caller EF state is isolated from saving the operation. Pre-commit recovery restores Areas, siblings, destination absence and caller pending work; later saves cannot resurrect staged changes. The current locked Trip is stamped by property through a narrow save path with nondecreasing freshness, preserving independent Trip metadata. This guarantee applies to this new operation; #853 retains ownership of broader writer correction.

Unknown outcomes and editor behavior

Vue never probes with DELETE or automatically replays a mutation. Each selection change invalidates the token; superseded previews cannot replace current evidence. Final activation is disabled until a matching fresh preview arrives. Cancellation before dispatch performs no mutation and leaves drafts intact. Successful slices apply only after returned effect identities and affected scope are checked. Only the same unchanged deleted Region draft/selection is cleared; newer sessions/intent and unrelated drafts remain intact.

Before dispatch, Vue retains minimal intent in local browser storage scoped to server/account/Trip/source. It includes dispositions and an attempt ID, without credentials, authored child content or confirmation token. Lost/unclassified responses show “Outcome unknown — refresh to inspect the current Trip.” A refresh can show facts, but does not establish the earlier request’s terminal state or release the hold. A matched success clears it; otherwise explicit terminal-state reconciliation is required. No durable receipt/status API is added. Maintainers must establish terminality from authoritative request evidence before clearing a held browser intent; absence of a Region alone is insufficient.

Moved Areas retain edit/delete/search/map access. Existing owned Areas can be ordered within the canonical Region using complete-membership validation. Direct Area creation and the canonical Region’s metadata/reorder/delete remain protected. Issue #847 separately owns Unassigned Place endpoint/waypoint picker eligibility; full integrated Segment picker UX acceptance remains dependent on that issue.

BD2-13 reuses this delivered four-way matrix and conservative content consent, including kept children. Its current protected-writer integration and fresh versus reused qualification boundary are documented with BD2-12/13.

Back to top