{"schemaVersion":"mappls.journey-workshop.v1","slug":"location-capture-evidence","journeySlug":"location-capture-evidence","title":"Build Consent-bound location capture evidence","summary":"An eight-lab, source-bounded workshop for the complete location capture attempt 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":"capture-feedback","stateModel":"hybrid","aggregate":"location capture attempt","actorCount":5,"stateCount":8,"transitionCount":9,"eventCount":9,"sourceGuideSlugs":["mappls-location-capture-android-sdk","mappls-location-capture-ios-sdk","mappls-location-capture-sdk-ios-distribution"],"contractSlugs":[],"relatedTutorialSlugs":[],"sample":{"slug":"address-verifier","name":"Address Verifier","downloadPath":"/downloads/address-verifier.zip","checksumPath":"/downloads/address-verifier.zip.sha256","verifiedTestCount":8,"runCommand":"npm test --workspace @mappls-example/address-verifier"},"labs":[{"slug":"model-lifecycle","title":"Model the lifecycle before the UI","duration":"15 min","objective":"Turn the location capture attempt blueprint into an explicit aggregate boundary owned by the application.","build":["draft: The host owns a business purpose, subject or case identity, capture policy, and retention class before requesting device access.","permission_pending: The user is deciding the minimum native permission for the declared purpose and visible capture behavior.","ready: SDK initialization, entitlement, permission, policy validation, and one launch generation have succeeded.","acquiring: Exactly one single-shot request or bounded subscription owns timeout, accuracy, distance, packet-size, callback, and cleanup responsibility.","candidate: A returned event has normalized coordinates, horizontal accuracy, event time, receipt time, policy result, and source generation, but is not yet accepted evidence.","review_pending: The candidate is usable only with human review because accuracy, freshness, or policy confidence is below the automatic threshold.","accepted: An actor accepted one immutable normalized fix for the declared purpose and policy version, with an audit event and retention deadline.","cancelled: The user or host ended the attempt and every active SDK resource was stopped or unsubscribed."],"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_attempt by Host application: new aggregate → draft; emit location_capture.attempt_created.","request_permission by Application user: draft → permission_pending; emit location_capture.permission_requested.","prepare_sdk by Platform adapter: permission_pending → ready; emit location_capture.sdk_ready.","start_acquisition by Application user: ready | review_pending → acquiring; emit location_capture.acquisition_started.","receive_location by Platform adapter: acquiring → candidate; emit location_capture.candidate_received.","queue_review by Host application: candidate → review_pending; emit location_capture.review_queued.","accept_evidence by Application user: candidate → accepted; emit location_capture.evidence_accepted.","approve_weak_evidence by Evidence reviewer: review_pending → accepted; emit location_capture.weak_evidence_approved.","cancel_attempt by Application user: draft | permission_pending | ready | acquiring | candidate | review_pending → cancelled; emit location_capture.attempt_cancelled."],"prove":["create_attempt resolves to location-capture-evidence-location-capture-attempt-created without claiming a provider webhook payload.","request_permission resolves to location-capture-evidence-location-capture-permission-requested without claiming a provider webhook payload.","prepare_sdk resolves to location-capture-evidence-location-capture-sdk-ready without claiming a provider webhook payload.","start_acquisition resolves to location-capture-evidence-location-capture-acquisition-started without claiming a provider webhook payload.","receive_location resolves to location-capture-evidence-location-capture-candidate-received without claiming a provider webhook payload.","queue_review resolves to location-capture-evidence-location-capture-review-queued without claiming a provider webhook payload.","accept_evidence resolves to location-capture-evidence-location-capture-evidence-accepted without claiming a provider webhook payload.","approve_weak_evidence resolves to location-capture-evidence-location-capture-weak-evidence-approved without claiming a provider webhook payload.","cancel_attempt resolves to location-capture-evidence-location-capture-attempt-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":["Capture attempt: Current host-owned purpose, policy, lifecycle state, generation, and optimistic version. Keys: attemptId, externalId, purpose, policyVersion, state, generation, version.","Normalized location evidence: Immutable accuracy-bearing observation independent of the SDK object's lifetime. Keys: evidenceId, latitude, longitude, horizontalAccuracy, eventTime, receivedTime, sourceGeneration, contentHash.","Review decision: Attributable disposition of evidence that cannot be accepted automatically. Keys: reviewId, evidenceId, reviewer, decision, reason, decidedAt.","Audit and outbox: Append-only transitions and exactly-once-in-effect downstream notification. Keys: eventId, aggregateVersion, actor, idempotencyKey, 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":["A permission grant is not consent for every purpose; purpose, policy, and retention are recorded separately.","One acquisition generation owns one callback family, timeout, stop, and unsubscribe lifecycle.","Accuracy, event time, receipt time, and policy result remain attached to the normalized coordinates.","A callback creates a candidate, never an accepted business decision.","Weak or stale evidence cannot pass an automatic acceptance threshold by omitting quality fields.","Credentials, native manager instances, full opaque payloads, and callback closures never enter durable storage."],"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":["Permission is denied or restricted: detect with The native authorization result cannot satisfy the declared capture mode. Recover with Explain the affected outcome, offer settings or a manual fallback where appropriate, and keep the attempt non-acquiring.","Initialization or entitlement fails: detect with The documented initialize operation returns failure before the generation becomes ready. Recover with Expose a safe configuration or entitlement error, retain no credential value, and require a deliberate retry after correction.","Acquisition times out or misses the accuracy budget: detect with The configured deadline expires or every event remains outside policy. Recover with Stop or unsubscribe once, preserve the quality reason, and route to review, retry, or fallback rather than fabricating a precise fix.","A callback arrives after cancellation or screen disposal: detect with The owner is inactive or callback generation differs from the current attempt generation. Recover with Ignore the event, perform idempotent cleanup, and do not change the terminal or newer attempt.","The app restarts during review: detect with The immutable candidate exists but the review command lacks acknowledgement. Recover with Reload the attempt and replay the same command key; never reacquire or duplicate evidence merely to recover workflow state."],"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 Address Verifier 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":["Initialization and configuration outcomes by SDK platform and released version","Permission denied, restricted, and settings-return rate","Time to first event and time to policy-acceptable event","Accuracy and freshness distributions without high-cardinality coordinate labels","Single-shot, subscription, timeout, stop, unsubscribe, and late-callback counts","Automatic acceptance, review, retry, fallback, and cancellation rate","Idempotency replay and optimistic version conflict rate","Outbox backlog, retry, and dead-letter age"],"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\" | \"permission_pending\" | \"ready\" | \"acquiring\" | \"candidate\" | \"review_pending\" | \"accepted\" | \"cancelled\";\ntype CommandName = \"create_attempt\" | \"request_permission\" | \"prepare_sdk\" | \"start_acquisition\" | \"receive_location\" | \"queue_review\" | \"accept_evidence\" | \"approve_weak_evidence\" | \"cancel_attempt\";\n\ntype Command = {\n  name: CommandName;\n  aggregateId: string;\n  expectedVersion: number;\n  idempotencyKey: string;\n};\n\nconst transitions = {\n  \"create_attempt\": { from: [null], to: \"draft\", event: \"location_capture.attempt_created\" },\n  \"request_permission\": { from: [\"draft\"], to: \"permission_pending\", event: \"location_capture.permission_requested\" },\n  \"prepare_sdk\": { from: [\"permission_pending\"], to: \"ready\", event: \"location_capture.sdk_ready\" },\n  \"start_acquisition\": { from: [\"ready\", \"review_pending\"], to: \"acquiring\", event: \"location_capture.acquisition_started\" },\n  \"receive_location\": { from: [\"acquiring\"], to: \"candidate\", event: \"location_capture.candidate_received\" },\n  \"queue_review\": { from: [\"candidate\"], to: \"review_pending\", event: \"location_capture.review_queued\" },\n  \"accept_evidence\": { from: [\"candidate\"], to: \"accepted\", event: \"location_capture.evidence_accepted\" },\n  \"approve_weak_evidence\": { from: [\"review_pending\"], to: \"accepted\", event: \"location_capture.weak_evidence_approved\" },\n  \"cancel_attempt\": { from: [\"draft\", \"permission_pending\", \"ready\", \"acquiring\", \"candidate\", \"review_pending\"], to: \"cancelled\", event: \"location_capture.attempt_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_location_capture_evidence (\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_location_capture_evidence_commands (\n  aggregate_id text NOT NULL REFERENCES journey_location_capture_evidence(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_location_capture_evidence_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\":\"location-capture-evidence\",\"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\":\"location-capture-evidence\",\"scenario\":\"unknown-outcome\"}'\n\n# Reconcile locationcaptureattempt 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_attempt\",\n  \"aggregateId\": \"fixture-location-capture-evidence-001\",\n  \"expectedVersion\": 0,\n  \"idempotencyKey\": \"cmd_location-capture-evidence_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=location-capture-evidence&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=location-capture-evidence&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=location-capture-evidence&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=location-capture-evidence&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=location-capture-evidence&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 Address Verifier 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/location-capture-evidence/workshop","apiPath":"/api/journey-workshops?journey=location-capture-evidence","providerCalls":0,"writesExposed":false}