# Module 5 assignment: event-driven travel recovery assistant

Build a small end-to-end distributed application that detects a disrupted trip,
finds alternatives, proposes a plan, and executes a mock rebooking only after
human approval. This is a public implementation brief. Canvas defines official
deadlines, grading, submission rules, and team requirements.

## Scope and preparation

No previous course project is required. Start in a new directory. Use Docker
with Compose v2, a language you know, and a Kafka client for that language.
The Module 5 classroom ZIP supplies a standalone Kafka broker and CLI examples;
it is broker/demo infrastructure, not a completed solution to this assignment.
Add your own application containers, HTTP endpoints, durable state, and tests.
If using that Compose broker from other containers, configure an advertised
listener reachable as `kafka:9092`; the CLI exercise's localhost-only defaults
are intended for clients running inside the broker container.

Use local Apache Kafka, synthetic travel data, and mock provider APIs. No
Confluent account, paid model API, real booking account, or credit card is
required. Implement a deterministic planner with the same structured interface
an LLM would use. Label it a mock agent: it validates the distributed workflow,
not AI reasoning quality. An LLM planner is an optional extension behind that
interface; preserve all deterministic checks and approval requirements.

## The scenario and business invariant

Trip t42 has a cancelled flight. Two alternatives are available: F101 costs
240 USD and arrives at 17:00; F102 costs 180 USD and arrives at 19:00. The
traveler's additional-spend limit is 250 USD and latest arrival is 18:00.
The planner must propose F101 and explain why F102 violates the arrival limit.
If no option satisfies both constraints, request human review rather than
inventing an itinerary. All times use the same explicitly documented timezone.

**Invariant:** at most one mock booking is created for an approved proposal
version, and none is created without approval of that exact version. A new
proposal requires new approval. A cancellation or event redelivery must not
silently authorize spending.

## Components and interaction style

| Component                 | Responsibility                                             | Communication                               |
| ------------------------- | ---------------------------------------------------------- | ------------------------------------------- |
| Trip API / event injector | Accept synthetic disruption and expose status              | HTTP request/response; outbox to Kafka      |
| Planner worker            | Consume disruption, query options, choose a valid proposal | Kafka input/output; HTTP RPC to mock search |
| Mock provider             | Return options and perform idempotent mock bookings        | HTTP RPC; durable booking records           |
| Approval API              | Display proposal/version and record approve/reject         | HTTP; outbox to Kafka                       |
| Executor worker           | Validate approval and perform one authorized action        | Kafka input/output; HTTP RPC to provider    |
| Audit consumer            | Build a read-only timeline                                 | Separate Kafka consumer group               |

The planner and executor are independent workers with explicit goals and tool
interfaces. Kafka transports events and permits independent progress. Your
application owns planning, workflow state, authorization, and recovery.
A CLI is sufficient; no web UI is required.

## Event contracts

Create four topics: `travel.disruptions`, `travel.proposals`, `travel.approvals`,
and `travel.outcomes`. Use tripId as the record key. Use separate groups for
planner, executor, and audit. Ordering is only within a partition of one topic;
there is no cross-topic ordering guarantee. Persist state and validate versions
rather than relying on arrival order across topics.

Every envelope includes `eventId`, `eventType`, `schemaVersion`, `tripId`,
`correlationId`, `causationId`, and `occurredAt`. Stable event IDs survive
publication retries; different facts receive different IDs. `correlationId`
connects the complete recovery attempt, while `causationId` references the
triggering event (null for the original disruption).

```json
{
  "eventId": "e-proposal-1",
  "eventType": "RebookingProposed",
  "schemaVersion": 1,
  "tripId": "t42",
  "correlationId": "recovery-t42-1",
  "causationId": "e-disruption-1",
  "occurredAt": "2026-09-28T20:00:00Z",
  "proposalId": "p42",
  "proposalVersion": 1,
  "flightId": "F101",
  "additionalCost": 240,
  "currency": "USD",
  "reason": "Within budget and arrives before 18:00"
}
```

Define `TripDisrupted`, `RebookingProposed`, `ProposalApproved` /
`ProposalRejected`, and `BookingConfirmed` / `BookingFailed` /
`BookingOutcomeUnknown` schemas. Approval identifies proposalId and version;
it does not supply an unchecked replacement itinerary. The executor retrieves
the stored approved proposal and revalidates its constraints. Handle stale
approvals with a visible rejection outcome. Keep sensitive traveler data out
of event payloads.

## Build sequence

1. **Bootstrap:** provide a Compose file and one documented startup command.
   Add broker readiness checks and topic initialization. Keep data for each
   service in its own durable store (SQLite with volumes is sufficient).
2. **Accept a disruption:** `POST /trips/t42/disruptions` stores trip state and
   an outbox entry in one transaction. A relay publishes `TripDisrupted` and
   retries without changing its eventId.
3. **Plan:** the planner consumes the event, calls
   `GET /options?tripId=t42` with a deadline, and chooses a valid alternative.
   Store processed event ID, proposal, and output outbox entry atomically.
   Commit consumed progress only after durable state commits. Bound retries;
   report a human-review state if search is unavailable or no option fits.
4. **Approve:** `GET /trips/t42` shows the proposed itinerary and version.
   `POST /proposals/p42/approve` with version 1 records approval and emits the
   approval event through an outbox. Rejection must cause no booking.
5. **Execute:** consume approval, verify the stored proposal/version and
   authorization, then call `POST /bookings` with idempotency key `p42:v1`.
   Persist the executor operation identity before the call. The mock provider
   enforces a unique key and returns the same booking for repeated calls.
   After a timeout, query `GET /bookings/by-key/p42:v1` before deciding whether
   to retry or mark an unknown outcome. Persist outcome and outbox together.
6. **Observe:** `GET /trips/t42` displays confirmed, failed, or unresolved state.
   Audit builds a timeline from all topics without invoking any booking tool.
   Start a fresh audit group to rebuild it from retained events.

For event publication, consumer effect, and external RPC, show exactly where
state is durable and what happens after a crash. Do not claim a Kafka offset
commit makes the provider call atomic. A local deduplication record alone
cannot prevent a repeat remote effect after a crash.

## Acceptance experiments

| Experiment                                                       | Required observation                                                                |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| Happy path                                                       | Disruption → F101 proposal → human approval → one booking → audit timeline          |
| Reject proposal                                                  | Rejected status; provider booking count remains zero                                |
| Duplicate disruption or approval                                 | No second logical proposal for the same disruption/version and no duplicate booking |
| Provider books but response times out                            | Lookup by p42:v1 finds the booking; final state and count agree                     |
| Restart executor after provider success, before local completion | Same operation key recovers one booking, not a new key                              |
| Approve version 1 after proposal changes to version 2            | Stale approval rejected; no booking for version 2 without new approval              |
| Search timeout / no valid option                                 | Bounded attempts and visible needs-review state; no invented result                 |
| Replay with a fresh audit group                                  | Timeline rebuilds; provider booking count does not change                           |

Record event IDs, proposal versions, group offsets, retry counts, and provider
booking count. Kafka retention must cover the audit replay window. Replay must
be read-only for this assignment; executor groups are not reset for audit.

## What to prepare

- Source, Compose file, topic setup, and README with exact startup/demo/reset commands.
- A sequence diagram labeling Kafka events, synchronous RPC, approval, and durable writes.
- A contract table and explanation of message ID versus trip ID versus operation key.
- Evidence for each acceptance experiment, including expected versus observed outcomes.
- A short design note explaining why Kafka, RPC, and durable state are all needed here,
  and when a conventional work queue would be simpler.

Optional: replace the mock planner with an LLM producing a validated proposal
schema. Treat model output as untrusted input; it cannot change approval rules
or call the booking provider directly. Compare reasoning quality separately
from delivery/recovery correctness. Do not require students to purchase access.
