{"schemaVersion":"mappls.journey-workshop.v1","slug":"field-service-task","journeySlug":"field-service-task","title":"Build Field-service task lifecycle","summary":"An eight-lab, source-bounded workshop for the complete task 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":"workmate","stateModel":"stateful","aggregate":"task","actorCount":4,"stateCount":8,"transitionCount":8,"eventCount":8,"sourceGuideSlugs":["mapmyindia-workmate-apis","mappls-workmate-android-sdk"],"contractSlugs":["workmate-post-tasks-to-create-new-task","workmate-get-tasks-taskid-to-get-the-task-details-for-the-given-task-id","workmate-put-tasks-taskid-to-update-the-task-description-status","workmate-get-users-to-get-all-the-user-details-which-belongs-to-your-organization","workmate-get-clients-to-get-all-your-client-details"],"relatedTutorialSlugs":[],"sample":{"slug":"field-service","name":"Field Service Console","downloadPath":"/downloads/field-service.zip","checksumPath":"/downloads/field-service.zip.sha256","verifiedTestCount":6,"runCommand":"npm test --workspace @mappls-example/field-service"},"labs":[{"slug":"model-lifecycle","title":"Model the lifecycle before the UI","duration":"15 min","objective":"Turn the task blueprint into an explicit aggregate boundary owned by the application.","build":["unassigned: The job exists with client, site, SLA, skills, window, and proof policy but no worker owns it.","assigned: A specific eligible worker owns the next decision and dispatch has recorded why they were selected.","accepted: The worker has acknowledged responsibility and the customer-facing plan can become firm.","en_route: Travel has begun and ETA, route deviation, and SLA-risk observations may change continuously.","in_progress: Arrival is established and work evidence can be gathered under the declared proof policy.","proof_pending: The worker submitted an immutable evidence set awaiting automated or supervisor validation.","completed: Required proof is accepted and downstream billing, inventory, SLA, and customer workflows may run.","cancelled: A named actor stopped the job with a reason before completion."],"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_task by Integration service: new aggregate → unassigned; emit task.created.","assign by Dispatcher: unassigned | assigned → assigned; emit task.assigned.","accept by Field worker: assigned → accepted; emit task.accepted.","start_travel by Field worker: accepted → en_route; emit task.en_route.","arrive by Field worker: en_route → in_progress; emit task.started.","submit_proof by Field worker: in_progress → proof_pending; emit task.proof_submitted.","approve_proof by Supervisor: proof_pending → completed; emit task.completed.","reject_proof by Supervisor: proof_pending → in_progress; emit task.proof_rejected."],"prove":["create_task resolves to field-service-task-task-created without claiming a provider webhook payload.","assign resolves to field-service-task-task-assigned without claiming a provider webhook payload.","accept resolves to field-service-task-task-accepted without claiming a provider webhook payload.","start_travel resolves to field-service-task-task-en-route without claiming a provider webhook payload.","arrive resolves to field-service-task-task-started without claiming a provider webhook payload.","submit_proof resolves to field-service-task-task-proof-submitted without claiming a provider webhook payload.","approve_proof resolves to field-service-task-task-completed without claiming a provider webhook payload.","reject_proof resolves to field-service-task-task-proof-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":["Task snapshot: Fast current-state reads and optimistic concurrency. Keys: taskId, externalId, state, version, assigneeId, mapplsPin.","Audit event: Attributable, replayable history for support and compliance. Keys: eventId, aggregateVersion, actor, commandId, occurredAt.","Evidence manifest: Immutable references and hashes for checklist, media, signature, and consent. Keys: manifestId, taskVersion, captureTime, contentHash, retentionClass.","Transactional outbox: Reliable downstream billing, inventory, notification, and analytics delivery. Keys: outboxId, eventId, status, attempts, nextAttemptAt."],"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 upstream external job maps to one durable task aggregate.","Only the assigned worker can accept, travel, arrive, or submit proof unless an attributable override is recorded.","Every command carries tenant, actor, idempotency key, expected version, and occurrence time.","Completion is impossible until the declared proof policy passes.","Task history is append-only; corrections use new events or compensating work."],"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":["Create-task response times out: detect with No provider response but the upstream external ID and idempotency key are known. Recover with Query by known identity or repeat the same command; do not mint a new job ID.","Two dispatchers edit the same task: detect with The submitted expected version is older than the current aggregate version. Recover with Return conflict with current state; refresh context and require an intentional new command.","Worker is offline during proof capture: detect with Evidence exists locally but no server acknowledgement or event ID exists. Recover with Retain command ID, hashes, capture timestamps, and retry queue until the committed event is returned.","Downstream system is unavailable after completion: detect with Task is complete but its outbox entry remains pending or retrying. Recover with Retry outbox delivery independently; never reopen or re-complete the task to trigger side effects."],"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 Field Service Console 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 (6 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":["Command acceptance, rejection code, actor, task version, and latency","Time spent in each state and SLA-risk interval","Duplicate command rate and version-conflict rate","Offline queue age and proof upload completeness","Outbox backlog, attempt count, and dead-letter age","Precise-location access with purpose and retention class"],"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 = \"unassigned\" | \"assigned\" | \"accepted\" | \"en_route\" | \"in_progress\" | \"proof_pending\" | \"completed\" | \"cancelled\";\ntype CommandName = \"create_task\" | \"assign\" | \"accept\" | \"start_travel\" | \"arrive\" | \"submit_proof\" | \"approve_proof\" | \"reject_proof\";\n\ntype Command = {\n  name: CommandName;\n  aggregateId: string;\n  expectedVersion: number;\n  idempotencyKey: string;\n};\n\nconst transitions = {\n  \"create_task\": { from: [null], to: \"unassigned\", event: \"task.created\" },\n  \"assign\": { from: [\"unassigned\", \"assigned\"], to: \"assigned\", event: \"task.assigned\" },\n  \"accept\": { from: [\"assigned\"], to: \"accepted\", event: \"task.accepted\" },\n  \"start_travel\": { from: [\"accepted\"], to: \"en_route\", event: \"task.en_route\" },\n  \"arrive\": { from: [\"en_route\"], to: \"in_progress\", event: \"task.started\" },\n  \"submit_proof\": { from: [\"in_progress\"], to: \"proof_pending\", event: \"task.proof_submitted\" },\n  \"approve_proof\": { from: [\"proof_pending\"], to: \"completed\", event: \"task.completed\" },\n  \"reject_proof\": { from: [\"proof_pending\"], to: \"in_progress\", event: \"task.proof_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_field_service_task (\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_field_service_task_commands (\n  aggregate_id text NOT NULL REFERENCES journey_field_service_task(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_field_service_task_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\":\"field-service-task\",\"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\":\"field-service-task\",\"scenario\":\"unknown-outcome\"}'\n\n# Reconcile task 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_task\",\n  \"aggregateId\": \"fixture-field-service-task-001\",\n  \"expectedVersion\": 0,\n  \"idempotencyKey\": \"cmd_field-service-task_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=field-service-task&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=field-service-task&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=field-service-task&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=field-service-task&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=field-service-task&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 Field Service Console capstone passes 6 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/field-service-task/workshop","apiPath":"/api/journey-workshops?journey=field-service-task","providerCalls":0,"writesExposed":false}