{"schemaVersion":"mappls.journey-workshop.v1","slug":"durable-weekend-itinerary","journeySlug":"durable-weekend-itinerary","title":"Build Durable multi-stop itinerary","summary":"An eight-lab, source-bounded workshop for the complete trip itinerary lifecycle: exact commands and events, durable records, replay, concurrency, unknown outcomes, hostile fixtures, a maintained capstone, and production exit evidence.","duration":"2 hr 10 min","level":"Advanced","productSlug":"routes-navigation","stateModel":"hybrid","aggregate":"trip itinerary","actorCount":4,"stateCount":6,"transitionCount":9,"eventCount":9,"sourceGuideSlugs":["mappls-rest-apis"],"contractSlugs":["core-location-get-api-places-search-json-autosuggest-api","core-location-get-rest-key-resources-profile-geopositions-routing-api"],"relatedTutorialSlugs":[],"sample":{"slug":"trip-planner","name":"Weekend Trip Planner","downloadPath":"/downloads/trip-planner.zip","checksumPath":"/downloads/trip-planner.zip.sha256","verifiedTestCount":8,"runCommand":"npm test --workspace @mappls-example/trip-planner"},"labs":[{"slug":"model-lifecycle","title":"Model the lifecycle before the UI","duration":"15 min","objective":"Turn the trip itinerary blueprint into an explicit aggregate boundary owned by the application.","build":["draft: An ordered, bounded set of provider-backed places represents current trip intent without claiming a valid route.","planned: A route revision is bound to the exact ordered stop identities, travel profile, provider response, and planning time.","active: The traveller started the current route revision and each next stop receives an explicit visited or skipped outcome.","paused: An attributable interruption stops progress without discarding saved stops, outcomes, or route identity.","completed: Every stop is visited or explicitly skipped and the terminal itinerary, route revision, outcomes, and event history are retained.","cancelled: A named actor ended the itinerary with a reason while retaining all committed place, route, and progress evidence."],"prove":["Every persisted state exists in the reviewed blueprint.","Terminal states reject ordinary forward commands.","Recovery text is operational guidance, not another hidden state."]},{"slug":"command-event-contract","title":"Implement every command and event pair","duration":"20 min","objective":"Make intent, actor authority, allowed source state, committed state, and emitted fact reviewable together.","build":["create_trip by Traveller: new aggregate → draft; emit trip.created.","add_or_reorder_stop by Traveller: draft | planned → draft; emit trip.sequence_changed.","preview_route by Mappls routing service: draft → planned; emit trip.route_previewed.","start_trip by Traveller: planned → active; emit trip.started.","pause_trip by Traveller: active → paused; emit trip.paused.","resume_trip by Traveller: paused → active; emit trip.resumed.","visit_or_skip_next by Traveller: active → active; emit trip.stop_completed.","complete_trip by Traveller: active → completed; emit trip.completed.","cancel_trip by Traveller: draft | planned | active | paused → cancelled; emit trip.cancelled."],"prove":["create_trip resolves to durable-weekend-itinerary-trip-created without claiming a provider webhook payload.","add_or_reorder_stop resolves to durable-weekend-itinerary-trip-sequence-changed without claiming a provider webhook payload.","preview_route resolves to durable-weekend-itinerary-trip-route-previewed without claiming a provider webhook payload.","start_trip resolves to durable-weekend-itinerary-trip-started without claiming a provider webhook payload.","pause_trip resolves to durable-weekend-itinerary-trip-paused without claiming a provider webhook payload.","resume_trip resolves to durable-weekend-itinerary-trip-resumed without claiming a provider webhook payload.","visit_or_skip_next resolves to durable-weekend-itinerary-trip-stop-completed without claiming a provider webhook payload.","complete_trip resolves to durable-weekend-itinerary-trip-completed without claiming a provider webhook payload.","cancel_trip resolves to durable-weekend-itinerary-trip-cancelled without claiming a provider webhook payload."]},{"slug":"durable-records","title":"Persist restart-safe records","duration":"15 min","objective":"Separate business identity, provider evidence, command receipts, immutable facts, audit, and downstream delivery.","build":["Trip aggregate: Business identity, date, party, lifecycle, current route revision, and optimistic version. Keys: tripId, state, version, date, partySize, routeRevision.","Itinerary stop: Ordered provider-backed place and progress outcome. Keys: mapplsPin, position, providerProvenance, status, completedAt, skipReason.","Route revision: Immutable preview for one exact intent version. Keys: routeId, revision, orderedPins, profile, legs, distance, duration, plannedAt.","Audit and outbox: Attributable changes and reliable downstream collaboration or notification. Keys: eventId, aggregateVersion, actor, commandId, outboxStatus."],"prove":["Process restart restores the same aggregate version and command result.","Opaque SDK or native UI objects are not durable records.","Provider evidence and application decisions remain distinguishable."]},{"slug":"concurrency-replay","title":"Make concurrency and replay deterministic","duration":"15 min","objective":"Apply optimistic expected versions and aggregate-scoped idempotency before executing effects.","build":["Every saved stop originated from provider-backed discovery and retains its Mappls Pin and provenance.","A Mappls Pin occurs at most once in an itinerary.","A route revision is valid only for the exact ordered stop list and constraints that produced it.","Only the next pending stop may receive a visit or skip outcome.","Completion is impossible while any stop remains pending.","Commands are idempotent and compare the expected trip version."],"prove":["An exact replay returns the first result without another event or version.","A reused key with different intent conflicts.","A stale expected version changes no durable truth."]},{"slug":"effects-reconciliation","title":"Control effects and unknown outcomes","duration":"15 min","objective":"Commit outbox intent atomically, execute effects outside the transaction, and reconcile ambiguous results.","build":["A collaborator edits after route preview: detect with Current stop identities or aggregate version differ from the route's intent version. Recover with Invalidate the route, show the edit, and require a new preview before start.","Routing succeeds but response is lost: detect with The same ordered intent and request identity has no committed route revision. Recover with Reconcile or repeat the same idempotent request; never attach a response to newer intent.","A venue becomes unavailable during the trip: detect with Traveller or fresh provider/business data marks the next stop unavailable. Recover with Record an explicit skip with reason, then offer a newly versioned replan from current context.","Application restarts mid-trip: detect with A durable active aggregate exists without current client state. Recover with Restore visit/skip progress and current revision, refresh stale operational data, and ask before resuming guidance."],"prove":["A timeout remains an unknown outcome until identity-based reconciliation completes.","Retries are bounded and preserve the original business and command identities.","Dead-letter or manual review retains the entire attempt history."]},{"slug":"hostile-scenarios","title":"Run all hostile fixture scenarios","duration":"15 min","objective":"Exercise the success path plus replay, concurrency, state, and response-loss failures without an account.","build":["Complete journey: Commit the shortest reviewed success path to the journey-specific operating target.","Idempotent replay: Repeat one command identity and prove that version, event identity, and side effects do not duplicate.","Stale version: Reject a command based on an outdated aggregate version without changing durable truth.","Invalid transition: Reject a known command when the current state does not permit it.","Unknown outcome recovery: Reconcile after a lost response, then replay the original command identity safely."],"prove":["All fixture checks pass for all five scenarios.","Rejected commands emit no event and do not increment version.","The fixture makes zero provider calls and exposes no write tool."]},{"slug":"maintained-capstone","title":"Trace the Weekend Trip Planner capstone","duration":"20 min","objective":"Follow the maintained source through domain rules, adapter seam, repository transaction, HTTP boundary, UI evidence, and restart test.","build":["Run the app's declared test suite (8 tests).","Run fixture mode without a credential.","Inspect audit and outbox evidence after each transition.","Restart the process and continue the same aggregate."],"prove":["The downloadable archive checksum verifies before execution.","The capstone covers the journey target without inventing provider completion.","Browser and HTTP surfaces report the same durable version."]},{"slug":"production-exit","title":"Qualify the real integration boundary","duration":"15 min","objective":"Replace only reviewed adapter seams and collect independent production evidence without weakening application invariants.","build":["Discovery-to-save rate and Mappls Pin continuity","Stop edits, duplicates, limits, and version conflicts","Route preview latency, failures, profiles, and revisions","Time from preview to start and stale-preview invalidation","Visited and skipped stops with reason","Active trips without recent progress","Idempotent replay, outbox backlog, and restart recovery"],"prove":["Exact product entitlement and regional behavior are validated separately.","Provider contract tests cover success, rejection, throttling, timeout, and unknown outcome.","Security, privacy, operations, rollback, and product owners approve exact evidence.","Fixture completion is never presented as provider or production completion."]}],"codeSamples":[{"language":"typescript","label":"TypeScript aggregate boundary","code":"type State = \"draft\" | \"planned\" | \"active\" | \"paused\" | \"completed\" | \"cancelled\";\ntype CommandName = \"create_trip\" | \"add_or_reorder_stop\" | \"preview_route\" | \"start_trip\" | \"pause_trip\" | \"resume_trip\" | \"visit_or_skip_next\" | \"complete_trip\" | \"cancel_trip\";\n\ntype Command = {\n  name: CommandName;\n  aggregateId: string;\n  expectedVersion: number;\n  idempotencyKey: string;\n};\n\nconst transitions = {\n  \"create_trip\": { from: [null], to: \"draft\", event: \"trip.created\" },\n  \"add_or_reorder_stop\": { from: [\"draft\", \"planned\"], to: \"draft\", event: \"trip.sequence_changed\" },\n  \"preview_route\": { from: [\"draft\"], to: \"planned\", event: \"trip.route_previewed\" },\n  \"start_trip\": { from: [\"planned\"], to: \"active\", event: \"trip.started\" },\n  \"pause_trip\": { from: [\"active\"], to: \"paused\", event: \"trip.paused\" },\n  \"resume_trip\": { from: [\"paused\"], to: \"active\", event: \"trip.resumed\" },\n  \"visit_or_skip_next\": { from: [\"active\"], to: \"active\", event: \"trip.stop_completed\" },\n  \"complete_trip\": { from: [\"active\"], to: \"completed\", event: \"trip.completed\" },\n  \"cancel_trip\": { from: [\"draft\", \"planned\", \"active\", \"paused\"], to: \"cancelled\", event: \"trip.cancelled\" },\n} as const;\n\nexport function decide(current: { state: State | null; version: number }, command: Command) {\n  const rule = transitions[command.name];\n  if (command.expectedVersion !== current.version) throw new Error(\"version_conflict\");\n  if (!rule.from.includes(current.state as never)) throw new Error(\"invalid_transition\");\n  return {\n    state: rule.to as State,\n    version: current.version + 1,\n    event: rule.event,\n    idempotencyKey: command.idempotencyKey,\n  };\n}\n\n// Persist the result, immutable event, audit row, and outbox intent atomically.\n// Store the first result by idempotencyKey before executing another effect."},{"language":"sql","label":"SQL durability skeleton","code":"CREATE TABLE journey_durable_weekend_itinerary (\n  aggregate_id text PRIMARY KEY,\n  state text NOT NULL,\n  version bigint NOT NULL CHECK (version > 0),\n  updated_at timestamptz NOT NULL DEFAULT now()\n);\n\nCREATE TABLE journey_durable_weekend_itinerary_commands (\n  aggregate_id text NOT NULL REFERENCES journey_durable_weekend_itinerary(aggregate_id),\n  idempotency_key text NOT NULL,\n  request_hash text NOT NULL CHECK (length(request_hash) = 64),\n  committed_version bigint NOT NULL,\n  result_json jsonb NOT NULL,\n  PRIMARY KEY (aggregate_id, idempotency_key)\n);\n\nCREATE TABLE journey_durable_weekend_itinerary_outbox (\n  event_id text PRIMARY KEY,\n  aggregate_id text NOT NULL,\n  aggregate_version bigint NOT NULL,\n  event_type text NOT NULL,\n  payload jsonb NOT NULL,\n  published_at timestamptz\n);\n\n-- In one transaction: lock aggregate, compare version, decide, append audit/event,\n-- insert the outbox row, and remember the exact command result."},{"language":"curl","label":"Complete fixture journey","code":"curl --request POST 'https://developer.mappls.com/api/journey-simulator' \\\n+  --header 'content-type: application/json' \\\n+  --data '{\"journey\":\"durable-weekend-itinerary\",\"scenario\":\"complete-journey\"}'"},{"language":"curl","label":"Unknown-outcome drill","code":"curl --request POST 'https://developer.mappls.com/api/journey-simulator' \\\n+  --header 'content-type: application/json' \\\n+  --data '{\"journey\":\"durable-weekend-itinerary\",\"scenario\":\"unknown-outcome\"}'\n\n# Reconcile tripitinerary identity and the original idempotency key.\n# Never mint a replacement key merely because the response was lost."},{"language":"json","label":"First command envelope","code":"{\n  \"command\": \"create_trip\",\n  \"aggregateId\": \"fixture-durable-weekend-itinerary-001\",\n  \"expectedVersion\": 0,\n  \"idempotencyKey\": \"cmd_durable-weekend-itinerary_001\",\n  \"evidenceBoundary\": \"application-owned-workshop\"\n}"}],"simulationScenarios":[{"slug":"complete-journey","title":"Complete journey","outcome":"Commit the shortest reviewed success path to the journey-specific operating target.","href":"/tools/journey-lab?journey=durable-weekend-itinerary&scenario=complete-journey#lab"},{"slug":"idempotent-replay","title":"Idempotent replay","outcome":"Repeat one command identity and prove that version, event identity, and side effects do not duplicate.","href":"/tools/journey-lab?journey=durable-weekend-itinerary&scenario=idempotent-replay#lab"},{"slug":"stale-version","title":"Stale version","outcome":"Reject a command based on an outdated aggregate version without changing durable truth.","href":"/tools/journey-lab?journey=durable-weekend-itinerary&scenario=stale-version#lab"},{"slug":"invalid-transition","title":"Invalid transition","outcome":"Reject a known command when the current state does not permit it.","href":"/tools/journey-lab?journey=durable-weekend-itinerary&scenario=invalid-transition#lab"},{"slug":"unknown-outcome","title":"Unknown outcome recovery","outcome":"Reconcile after a lost response, then replay the original command identity safely.","href":"/tools/journey-lab?journey=durable-weekend-itinerary&scenario=unknown-outcome#lab"}],"acceptance":["All 9 reviewed transitions are implemented with actor and source-state checks.","All 9 application event identities are immutable and versioned.","Exact replay, idempotency conflict, stale version, invalid transition, and unknown outcome are tested.","Aggregate, event, audit, command result, and outbox intent commit atomically.","The Weekend Trip Planner capstone passes 8 declared tests after archive checksum verification.","Provider entitlement, payload, callback, completion, and production behavior remain independently evidenced."],"sourceBoundary":["The journey blueprint and application event contracts are implementation guidance, not Mappls provider payload specifications.","Only linked normalized contracts and source guides may define provider request syntax; empty evidence is never backfilled.","The simulator and maintained capstone operate in explicit fixture mode and make no entitlement claim.","Credentials, precise production payloads, opaque native objects, and provider secrets stay outside workshop inputs and durable examples."],"releaseBoundary":"Workshop completion proves an application-owned reliability design only. Production still requires issued entitlement, exact adapter contract tests, regional and quota validation, security/privacy review, operational drills, and independent release approval.","websitePath":"/journeys/durable-weekend-itinerary/workshop","apiPath":"/api/journey-workshops?journey=durable-weekend-itinerary","providerCalls":0,"writesExposed":false}