{"schemaVersion":"mappls.journey-workshop.v1","slug":"consented-address-verification","journeySlug":"consented-address-verification","title":"Build Consented address verification","summary":"An eight-lab, source-bounded workshop for the complete address verification 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":"search-places","stateModel":"stateful","aggregate":"address verification","actorCount":5,"stateCount":9,"transitionCount":11,"eventCount":9,"sourceGuideSlugs":["mappls-rest-apis"],"contractSlugs":["core-location-get-api-places-search-json-autosuggest-api","core-location-get-rest-key-rev-geocode-reverse-geocode-api"],"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 address verification blueprint into an explicit aggregate boundary owned by the application.","build":["entered: One external business reference, opaque subject reference, declared purpose, and raw address intent are recorded.","normalized: The declared address maps to a provider-backed Mappls Pin, coordinate, components, confidence, and provenance.","capture_authorized: A specific subject granted one purpose-bound, expiring, accuracy- and retention-governed evidence capture.","evidence_captured: Immutable source identity, event and receipt time, coordinate, accuracy, provider context, and integrity hash are committed.","compared: A versioned policy records distance, effective uncertainty, thresholds, and its verify, reject, or review recommendation.","review_required: Weak, conflicting, or policy-sensitive evidence is assigned to an attributable human decision.","verified: A policy or human decision accepted the declared address for the exact recorded purpose.","rejected: Evidence did not establish the declared address, with reason, recommendation, actor, and appeal path retained.","cancelled: Consent was withdrawn before evidence capture and no precise observation may be accepted."],"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":["enter_verification by Application user: new aggregate → entered; emit address_verification.entered.","normalize_address by Policy service: entered → normalized; emit address_verification.normalized.","authorize_capture by Application user: normalized → capture_authorized; emit address_verification.capture_authorized.","revoke_consent by Application user: capture_authorized → cancelled; emit address_verification.consent_revoked.","capture_evidence by Evidence capture application: capture_authorized → evidence_captured; emit address_verification.evidence_captured.","compare by Policy service: evidence_captured → compared; emit address_verification.compared.","verify by Policy service: compared → verified; emit address_verification.verified.","reject by Policy service: compared → rejected; emit address_verification.rejected.","defer by Policy service: compared → review_required; emit address_verification.review_required.","review_verify by Human reviewer: review_required → verified; emit address_verification.verified.","review_reject by Human reviewer: review_required → rejected; emit address_verification.rejected."],"prove":["enter_verification resolves to consented-address-verification-address-verification-entered without claiming a provider webhook payload.","normalize_address resolves to consented-address-verification-address-verification-normalized without claiming a provider webhook payload.","authorize_capture resolves to consented-address-verification-address-verification-capture-authorized without claiming a provider webhook payload.","revoke_consent resolves to consented-address-verification-address-verification-consent-revoked without claiming a provider webhook payload.","capture_evidence resolves to consented-address-verification-address-verification-evidence-captured without claiming a provider webhook payload.","compare resolves to consented-address-verification-address-verification-compared without claiming a provider webhook payload.","verify resolves to consented-address-verification-address-verification-verified without claiming a provider webhook payload.","reject resolves to consented-address-verification-address-verification-rejected without claiming a provider webhook payload.","defer resolves to consented-address-verification-address-verification-review-required without claiming a provider webhook payload.","review_verify resolves to consented-address-verification-address-verification-verified without claiming a provider webhook payload.","review_reject resolves to consented-address-verification-address-verification-rejected 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":["Verification aggregate: Business identity, purpose, lifecycle, selected evidence, comparison, decision, and version. Keys: verificationId, externalReference, subjectReference, purpose, state, version.","Normalized address: Provider-backed place identity and address interpretation. Keys: mapplsPin, coordinate, formattedAddress, components, confidence, provenance.","Consent grant: Attributable authority and privacy bounds for precise capture. Keys: consentId, subject, purpose, textVersion, grantedAt, expiresAt, retentionUntil, status.","Evidence envelope: Immutable device observation with quality, timing, provenance, and integrity identity. Keys: evidenceId, sourceEventId, eventTime, receivedAt, coordinate, accuracy, contentHash.","Decision record: Explainable recommendation, human disposition, override, and appeal context. Keys: policyVersion, thresholds, recommendation, outcome, actor, reason."],"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":["One external business reference maps to one verification aggregate.","Precise evidence is accepted only under active consent for the exact declared purpose and time window.","Provider normalization and application policy are identified separately.","An automated actor cannot override its own policy recommendation.","Every terminal outcome retains evidence hash, policy version, actor, and attributable reason.","Precise evidence can be redacted without erasing the audit trail or claiming that retained hashes can reconstruct it."],"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":["Capture arrives after consent expiry or withdrawal: detect with Receipt or evidence event falls outside the committed grant window or grant status is revoked. Recover with Reject it without retaining precise payload and require a fresh consent for another attempt.","Device evidence is replayed: detect with Source-event identity or content hash already belongs to an accepted evidence envelope. Recover with Return the original result for an idempotent retry or reject conflicting reuse as a security event.","Address candidate is ambiguous or evidence accuracy is weak: detect with Provider confidence, device accuracy, separation, or policy combination enters the review band. Recover with Request clarification or independent review; do not transform uncertainty into a definitive match.","Retention deadline passes: detect with Precise evidence remains present beyond purpose, tenant, or jurisdiction policy. Recover with Redact coordinate and place payloads, retain the minimum decision and integrity record, and audit completion."],"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":["Normalization confidence, ambiguity, latency, and provider failures","Consent grant, expiry, withdrawal, and out-of-window capture attempts","Evidence accuracy, age, source integrity, replay, and mock-location risk","Distance and recommendation distribution by versioned policy","Review queue age, outcome, override rate, and supporting-evidence class","False-match, false-reject, correction, and appeal outcomes","Precise-data access, export, retention expiry, redaction, and legal hold","Idempotency conflicts, version conflicts, 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 = \"entered\" | \"normalized\" | \"capture_authorized\" | \"evidence_captured\" | \"compared\" | \"review_required\" | \"verified\" | \"rejected\" | \"cancelled\";\ntype CommandName = \"enter_verification\" | \"normalize_address\" | \"authorize_capture\" | \"revoke_consent\" | \"capture_evidence\" | \"compare\" | \"verify\" | \"reject\" | \"defer\" | \"review_verify\" | \"review_reject\";\n\ntype Command = {\n  name: CommandName;\n  aggregateId: string;\n  expectedVersion: number;\n  idempotencyKey: string;\n};\n\nconst transitions = {\n  \"enter_verification\": { from: [null], to: \"entered\", event: \"address_verification.entered\" },\n  \"normalize_address\": { from: [\"entered\"], to: \"normalized\", event: \"address_verification.normalized\" },\n  \"authorize_capture\": { from: [\"normalized\"], to: \"capture_authorized\", event: \"address_verification.capture_authorized\" },\n  \"revoke_consent\": { from: [\"capture_authorized\"], to: \"cancelled\", event: \"address_verification.consent_revoked\" },\n  \"capture_evidence\": { from: [\"capture_authorized\"], to: \"evidence_captured\", event: \"address_verification.evidence_captured\" },\n  \"compare\": { from: [\"evidence_captured\"], to: \"compared\", event: \"address_verification.compared\" },\n  \"verify\": { from: [\"compared\"], to: \"verified\", event: \"address_verification.verified\" },\n  \"reject\": { from: [\"compared\"], to: \"rejected\", event: \"address_verification.rejected\" },\n  \"defer\": { from: [\"compared\"], to: \"review_required\", event: \"address_verification.review_required\" },\n  \"review_verify\": { from: [\"review_required\"], to: \"verified\", event: \"address_verification.verified\" },\n  \"review_reject\": { from: [\"review_required\"], to: \"rejected\", event: \"address_verification.rejected\" },\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_consented_address_verification (\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_consented_address_verification_commands (\n  aggregate_id text NOT NULL REFERENCES journey_consented_address_verification(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_consented_address_verification_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\":\"consented-address-verification\",\"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\":\"consented-address-verification\",\"scenario\":\"unknown-outcome\"}'\n\n# Reconcile addressverification 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\": \"enter_verification\",\n  \"aggregateId\": \"fixture-consented-address-verification-001\",\n  \"expectedVersion\": 0,\n  \"idempotencyKey\": \"cmd_consented-address-verification_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=consented-address-verification&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=consented-address-verification&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=consented-address-verification&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=consented-address-verification&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=consented-address-verification&scenario=unknown-outcome#lab"}],"acceptance":["All 11 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/consented-address-verification/workshop","apiPath":"/api/journey-workshops?journey=consented-address-verification","providerCalls":0,"writesExposed":false}