{"schemaVersion":"mappls.journey-workshop.v1","slug":"place-contribution-publication","journeySlug":"place-contribution-publication","title":"Build Governed place contribution and publication","summary":"An eight-lab, source-bounded workshop for the complete place contribution 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":"stateful","aggregate":"place contribution","actorCount":4,"stateCount":7,"transitionCount":8,"eventCount":8,"sourceGuideSlugs":["mappls-app-widgets","mappls-rest-apis"],"contractSlugs":["core-location-get-api-places-search-json-autosuggest-api","core-location-get-apis-o2o-entity-eloc-place-detail-api"],"relatedTutorialSlugs":[],"sample":{"slug":"place-contribution-desk","name":"Place Contribution Desk","downloadPath":"/downloads/place-contribution-desk.zip","checksumPath":"/downloads/place-contribution-desk.zip.sha256","verifiedTestCount":8,"runCommand":"npm test --workspace @mappls-example/place-contribution-desk"},"labs":[{"slug":"model-lifecycle","title":"Model the lifecycle before the UI","duration":"15 min","objective":"Turn the place contribution blueprint into an explicit aggregate boundary owned by the application.","build":["draft: The host owns a bounded contribution intent, business purpose, and external identity before any provider surface opens.","widget_open: The Mappls-hosted form is visible for one recorded attempt, while provider UI and submission remain outside the host contract.","submission_reported: The contributor says the hosted form showed success, which is useful testimony but not a receipt or publication result.","publication_pending: An operations process is checking supported Mappls search or an approved provider receipt for a stable published identity.","published: A supported provider surface returned a six-character Mappls Pin with attributable observation evidence.","rejected: A reviewer found a duplicate, invalid, unsafe, or otherwise non-publishable contribution and recorded why.","withdrawn: The host stopped its own follow-up workflow at the contributor's request without claiming that the provider submission was deleted."],"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_contribution by Host application: new aggregate → draft; emit place_contribution.created.","open_widget by Contributor: draft → widget_open; emit place_contribution.widget_opened.","report_submission by Contributor: widget_open → submission_reported; emit place_contribution.submission_reported.","queue_reconciliation by Host application: submission_reported → publication_pending; emit place_contribution.reconciliation_queued.","confirm_publication by Operations reviewer: submission_reported | publication_pending → published; emit place_contribution.published.","reject by Operations reviewer: submission_reported | publication_pending → rejected; emit place_contribution.rejected.","retry by Contributor: rejected → widget_open; emit place_contribution.retried.","withdraw by Contributor: draft | widget_open | submission_reported | publication_pending → withdrawn; emit place_contribution.withdrawn."],"prove":["create_contribution resolves to place-contribution-publication-place-contribution-created without claiming a provider webhook payload.","open_widget resolves to place-contribution-publication-place-contribution-widget-opened without claiming a provider webhook payload.","report_submission resolves to place-contribution-publication-place-contribution-submission-reported without claiming a provider webhook payload.","queue_reconciliation resolves to place-contribution-publication-place-contribution-reconciliation-queued without claiming a provider webhook payload.","confirm_publication resolves to place-contribution-publication-place-contribution-published without claiming a provider webhook payload.","reject resolves to place-contribution-publication-place-contribution-rejected without claiming a provider webhook payload.","retry resolves to place-contribution-publication-place-contribution-retried without claiming a provider webhook payload.","withdraw resolves to place-contribution-publication-place-contribution-withdrawn 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":["Contribution aggregate: Current host-owned state, business identity, purpose, ownership, and optimistic version. Keys: contributionId, externalId, state, version, purpose, owner.","Widget attempt: Immutable record of each frame launch and contributor-reported outcome without provider-internal data. Keys: attemptId, aggregateVersion, openedAt, reportedAt, documentedSourceUrl.","Publication evidence: Attributable supported-source proof that a stable Mappls identity is observable. Keys: mapplsPin, evidenceSource, observedAt, sourceFingerprint, reviewer.","Audit and outbox: Append-only transitions and exactly-once-in-effect downstream notifications. 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 hosted success screen or contributor report never proves publication.","No callback, browser message, receipt, moderation status, or withdrawal capability is invented when the public source does not document it.","Only provider-backed evidence containing a valid Mappls Pin can close the aggregate as published.","Every widget attempt is immutable and linked to the aggregate version that opened it.","Commands are idempotent, compare expected version, and commit audit plus outbox atomically.","Contribution text, actor identity, and precise location follow declared purpose, access, and retention boundaries."],"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":["Hosted frame is blocked or unavailable: detect with The host cannot load the established HTTPS source within its timeout and CSP boundary. Recover with Keep the contribution in draft, explain the boundary, and offer an external open or later retry without claiming a submission.","User loses the success acknowledgement: detect with No provider receipt exists and the contributor cannot confirm what the hosted surface showed. Recover with Leave the attempt unresolved and allow a deliberate new attempt; never infer completion from iframe navigation.","Reconciliation finds an existing duplicate: detect with Supported search resolves the same place identity or a reviewer establishes duplicate ownership. Recover with Reject with duplicate reason and link the known Mappls Pin as context, not as evidence that this attempt created it.","No public result appears within the operating window: detect with Every bounded supported-source check returns no qualifying Mappls identity before the stated review deadline. Recover with Keep pending or reject according to published host policy and expose the last check time without promising a provider SLA.","Application restarts during review: detect with An in-flight command lacks acknowledgement while aggregate, command key, and outbox state are durable. Recover with Reload the aggregate and replay the same command key; do not duplicate an attempt, decision, or notification."],"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 Place Contribution Desk 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":["Widget opens, contributor reports, and abandonment by attempt","Time from report to first reconciliation and terminal decision","Pending age and last supported-source check","Publication evidence source and Mappls Pin validity","Duplicate and rejection reasons without opaque provider payloads","Withdrawal count explicitly separated from provider-side deletion","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\" | \"widget_open\" | \"submission_reported\" | \"publication_pending\" | \"published\" | \"rejected\" | \"withdrawn\";\ntype CommandName = \"create_contribution\" | \"open_widget\" | \"report_submission\" | \"queue_reconciliation\" | \"confirm_publication\" | \"reject\" | \"retry\" | \"withdraw\";\n\ntype Command = {\n  name: CommandName;\n  aggregateId: string;\n  expectedVersion: number;\n  idempotencyKey: string;\n};\n\nconst transitions = {\n  \"create_contribution\": { from: [null], to: \"draft\", event: \"place_contribution.created\" },\n  \"open_widget\": { from: [\"draft\"], to: \"widget_open\", event: \"place_contribution.widget_opened\" },\n  \"report_submission\": { from: [\"widget_open\"], to: \"submission_reported\", event: \"place_contribution.submission_reported\" },\n  \"queue_reconciliation\": { from: [\"submission_reported\"], to: \"publication_pending\", event: \"place_contribution.reconciliation_queued\" },\n  \"confirm_publication\": { from: [\"submission_reported\", \"publication_pending\"], to: \"published\", event: \"place_contribution.published\" },\n  \"reject\": { from: [\"submission_reported\", \"publication_pending\"], to: \"rejected\", event: \"place_contribution.rejected\" },\n  \"retry\": { from: [\"rejected\"], to: \"widget_open\", event: \"place_contribution.retried\" },\n  \"withdraw\": { from: [\"draft\", \"widget_open\", \"submission_reported\", \"publication_pending\"], to: \"withdrawn\", event: \"place_contribution.withdrawn\" },\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_place_contribution_publication (\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_place_contribution_publication_commands (\n  aggregate_id text NOT NULL REFERENCES journey_place_contribution_publication(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_place_contribution_publication_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\":\"place-contribution-publication\",\"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\":\"place-contribution-publication\",\"scenario\":\"unknown-outcome\"}'\n\n# Reconcile placecontribution 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_contribution\",\n  \"aggregateId\": \"fixture-place-contribution-publication-001\",\n  \"expectedVersion\": 0,\n  \"idempotencyKey\": \"cmd_place-contribution-publication_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=place-contribution-publication&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=place-contribution-publication&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=place-contribution-publication&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=place-contribution-publication&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=place-contribution-publication&scenario=unknown-outcome#lab"}],"acceptance":["All 8 reviewed transitions are implemented with actor and source-state checks.","All 8 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 Place Contribution Desk 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/place-contribution-publication/workshop","apiPath":"/api/journey-workshops?journey=place-contribution-publication","providerCalls":0,"writesExposed":false}