98. Design Workflow Automation Engine for Self Help Menu

Design Workflow Automation Engine
Design and implement a workflow automation engine for DoorDash-style order support automation. The system stores users, restaurants, and orders in memory, then reacts to order status updates with deterministic actions and logs.

In Part 1, implement the core workflow behavior: an order starts as OPEN, informational updates keep it open, ORDER_DELIVERED completes it, DELIVERY_CANCELLED cancels it, and closed orders should ignore later non-refund updates.

In Part 2, extend the same system so that different issues produce different compensation amounts. For example, late delivery may cause no refund, a 50% refund, or a 100% refund depending on how late it was, and a customer cancellation before preparation starts should return 95%.

The goal is to model clean order state transitions and return easy-to-test execution logs.

Workflow Rules

  • When an order is created, it starts in the OPEN state.
  • Informational statuses keep an OPEN order open: ORDER_GETTING_PREPARED, OUT_FOR_DELIVERY, RAIN_DELAY, DASHER_REACHED_DELIVERY_LOCATION, CUSTOMER_UNREACHABLE, and DASHER_UNREACHABLE.
  • When statusCode = ORDER_DELIVERED, the order becomes COMPLETED.
  • When statusCode = DELIVERY_CANCELLED, the order becomes CANCELLED.
  • When statusCode = WANT_REFUND and the order is OPEN, the order is cancelled after applying the refund rule.
  • Once an order is CANCELLED, later updates must not change it again.
  • Once an order is COMPLETED, later non-refund updates must be ignored.
  • At most one refund may be issued per order.

Compensation Rules

  • Late delivered order:
    • less than 10 minutes late → no refund
    • 10 to 20 minutes late, inclusive → 50% refund
    • more than 20 minutes late → 100% refund
  • Cancelled delivery → 100% refund
  • Customer refund request on an OPEN order:
    • if preparation has not started yet → 95% refund, then cancel the order
    • if preparation has already started → 100% refund, then cancel the order
  • Customer refund request on a COMPLETED order:
    • if refund was already issued → do not issue another refund
    • otherwise decide refund using the stored delivery lateness
  • Support agents do not override these rules in this version. The by field only affects logs.

Execution Log Format

  • Each call to updateOrderStatus() returns a List<String>.
  • Each string is one simple log line like KEY:VALUE.
  • The first three log lines are always in this exact order:
    • ORDER:<orderId>
    • BY:<by>
    • STATUS:<statusCode>
  • After that, add one or more result lines such as:
    • ACTION:CONTINUE_ORDER
    • ACTION:COMPLETE_ORDER
    • ACTION:CANCEL_ORDER
    • ACTION:ISSUE_REFUND
    • ACTION:IGNORED_ORDER_ALREADY_CLOSED
    • ACTION:REFUND_ALREADY_ISSUED
    • ACTION:NO_REFUND_RULE_MATCH
  • If LATE:true or LATE:false is present, it must appear immediately after the STATUS:... line.
  • If ACTION:ISSUE_REFUND is present, the corresponding REFUND_PERCENT:... line must appear immediately after it.
  • If a refund is issued, also include refund percent line must appear immediately after it.
    e.g. REFUND_PERCENT:50, REFUND_PERCENT:95, or REFUND_PERCENT:100.
  • Sample logs:

    List.of("ORDER:o1", "BY:DASHER", "STATUS:OUT_FOR_DELIVERY", "ACTION:CONTINUE_ORDER")

    List.of("ORDER:o3", "BY:DASHER", "STATUS:ORDER_DELIVERED", "LATE:true", "ACTION:COMPLETE_ORDER", "ACTION:ISSUE_REFUND", "REFUND_PERCENT:50")

Exact Behavior

Informational update on an OPEN order

  • Return:
    • ORDER:<orderId>
    • BY:<by>
    • STATUS:<statusCode>
    • ACTION:CONTINUE_ORDER
  • If statusCode = ORDER_GETTING_PREPARED, mark that preparation has started.

ORDER_DELIVERED on an OPEN order

  • Return:
    • ORDER:<orderId>
    • BY:<by>
    • STATUS:ORDER_DELIVERED
    • LATE:true if currentTime > deliveryETA, else LATE:false
    • ACTION:COMPLETE_ORDER
  • If refund applies and was not already issued, also add:
    • ACTION:ISSUE_REFUND
    • REFUND_PERCENT:50 or REFUND_PERCENT:100

DELIVERY_CANCELLED on an OPEN order

  • Return:
    • ORDER:<orderId>
    • BY:<by>
    • STATUS:DELIVERY_CANCELLED
    • ACTION:CANCEL_ORDER
    • ACTION:ISSUE_REFUND
    • REFUND_PERCENT:100

WANT_REFUND on an OPEN order

  • If preparation has not started, return:
    • ORDER:<orderId>
    • BY:<by>
    • STATUS:WANT_REFUND
    • ACTION:ISSUE_REFUND
    • REFUND_PERCENT:95
    • ACTION:CANCEL_ORDER
  • If preparation has started, return:
    • ORDER:<orderId>
    • BY:<by>
    • STATUS:WANT_REFUND
    • ACTION:ISSUE_REFUND
    • REFUND_PERCENT:100
    • ACTION:CANCEL_ORDER

WANT_REFUND on a COMPLETED order

  • If refund was already issued, return:
    • ORDER:<orderId>
    • BY:<by>
    • STATUS:WANT_REFUND
    • ACTION:REFUND_ALREADY_ISSUED
  • If refund was not already issued and the delivered order does not qualify for compensation, return:
    • ORDER:<orderId>
    • BY:<by>
    • STATUS:WANT_REFUND
    • ACTION:NO_REFUND_RULE_MATCH
  • If refund was not already issued and compensation applies, return:
    • ORDER:<orderId>
    • BY:<by>
    • STATUS:WANT_REFUND
    • ACTION:ISSUE_REFUND
    • REFUND_PERCENT:50 or REFUND_PERCENT:100

Non-refund update on a CLOSED order

  • Once an order is CANCELLED, its cancellation-time refund outcome is final. Later WANT_REFUND must not recalculate refund eligibility.
  • For any non-refund update on a COMPLETED or CANCELLED order, return:
    • ORDER:<orderId>
    • BY:<by>
    • STATUS:<statusCode>
    • ACTION:IGNORED_ORDER_ALREADY_CLOSED

Constructor

WorkflowAutomator(List<String> existingUsers, List<String> existingRestaurants)
  • Initializes the system with valid existing user ids and restaurant ids.
  • Each id is non-blank.
  • Every id across both lists is globally unique.

Methods

void createOrder(String orderId, String restaurantId, String userId, int deliveryETA, int currentTime)
  • Creates a new order.
  • deliveryETA and currentTime are minutes elapsed since 1 Jan 1970.
  • 1 <= currentTime < deliveryETA
  • orderId, restaurantId, and userId are valid and non-blank.
  • currentTime is monotonically non-decreasing across all method calls.
List<String> updateOrderStatus(String orderId, String by, int currentTime, String statusCode)
  • Applies one status update and returns execution logs.
  • by is one of CUSTOMER, DASHER, or SUPPORT_AGENT.
  • orderId always refers to a previously created order.
  • currentTime is monotonically non-decreasing across all method calls.
  • statusCode is a non-blank string such as ORDER_GETTING_PREPARED, OUT_FOR_DELIVERY, RAIN_DELAY, WANT_REFUND, ORDER_DELIVERED, DASHER_REACHED_DELIVERY_LOCATION, DELIVERY_CANCELLED, CUSTOMER_UNREACHABLE, or DASHER_UNREACHABLE.

Constraints

  • All ids and status strings are non-blank.
  • Each created orderId is unique.
  • All referenced users and restaurants are valid.
  • All calls to updateOrderStatus use an existing order.
  • currentTime is monotonically non-decreasing across the whole class.
  • All data must stay in memory.

Examples

Example 1: Informational update, late delivery with 50% refund, repeated refund request, and ignored closed update

Input

WorkflowAutomator automator = new WorkflowAutomator(existingUsers = List.of("u1", "u2"), existingRestaurants = List.of("r1", "r2"))

automator.createOrder(orderId = "o1", restaurantId = "r1", userId = "u1", deliveryETA = 100, currentTime = 80)

Method Call 1

automator.updateOrderStatus(orderId = "o1", by = "DASHER", currentTime = 85, statusCode = "OUT_FOR_DELIVERY")

Output 1

List.of("ORDER:o1", "BY:DASHER", "STATUS:OUT_FOR_DELIVERY", "ACTION:CONTINUE_ORDER")

Method Call 2

automator.updateOrderStatus(orderId = "o1", by = "DASHER", currentTime = 115, statusCode = "ORDER_DELIVERED")

Output 2

List.of("ORDER:o1", "BY:DASHER", "STATUS:ORDER_DELIVERED", "LATE:true", "ACTION:COMPLETE_ORDER", "ACTION:ISSUE_REFUND", "REFUND_PERCENT:50")

Method Call 3

automator.updateOrderStatus(orderId = "o1", by = "CUSTOMER", currentTime = 120, statusCode = "WANT_REFUND")

Output 3

List.of("ORDER:o1", "BY:CUSTOMER", "STATUS:WANT_REFUND", "ACTION:REFUND_ALREADY_ISSUED")

Method Call 4

automator.updateOrderStatus(orderId = "o1", by = "SUPPORT_AGENT", currentTime = 125, statusCode = "RAIN_DELAY")

Output 4

List.of("ORDER:o1", "BY:SUPPORT_AGENT", "STATUS:RAIN_DELAY", "ACTION:IGNORED_ORDER_ALREADY_CLOSED")

Example 2: Customer cancels before preparation starts and gets 95% refund

Input

WorkflowAutomator automator = new WorkflowAutomator(existingUsers = List.of("u1", "u2"), existingRestaurants = List.of("r1", "r2"))

automator.createOrder(orderId = "o2", restaurantId = "r2", userId = "u2", deliveryETA = 220, currentTime = 150)

Method Call 1

automator.updateOrderStatus(orderId = "o2", by = "CUSTOMER", currentTime = 155, statusCode = "WANT_REFUND")

Output 1

List.of("ORDER:o2", "BY:CUSTOMER", "STATUS:WANT_REFUND", "ACTION:ISSUE_REFUND", "REFUND_PERCENT:95", "ACTION:CANCEL_ORDER")

Method Call 2

automator.updateOrderStatus(orderId = "o2", by = "DASHER", currentTime = 160, statusCode = "OUT_FOR_DELIVERY")

Output 2

List.of("ORDER:o2", "BY:DASHER", "STATUS:OUT_FOR_DELIVERY", "ACTION:IGNORED_ORDER_ALREADY_CLOSED")

Example 3: Preparation started, then customer wants refund, so order is cancelled with 100% refund

Input

WorkflowAutomator automator = new WorkflowAutomator(existingUsers = List.of("u1", "u2"), existingRestaurants = List.of("r1", "r2"))

automator.createOrder(orderId = "o3", restaurantId = "r1", userId = "u1", deliveryETA = 300, currentTime = 200)

Method Call 1

automator.updateOrderStatus(orderId = "o3", by = "SUPPORT_AGENT", currentTime = 210, statusCode = "ORDER_GETTING_PREPARED")

Output 1

List.of("ORDER:o3", "BY:SUPPORT_AGENT", "STATUS:ORDER_GETTING_PREPARED", "ACTION:CONTINUE_ORDER")

Method Call 2

automator.updateOrderStatus(orderId = "o3", by = "CUSTOMER", currentTime = 215, statusCode = "WANT_REFUND")

Output 2

List.of("ORDER:o3", "BY:CUSTOMER", "STATUS:WANT_REFUND", "ACTION:ISSUE_REFUND", "REFUND_PERCENT:100", "ACTION:CANCEL_ORDER")

Example 4: Slightly late delivery gets no refund, and later refund request still does not qualify

Input

WorkflowAutomator automator = new WorkflowAutomator(existingUsers = List.of("u1", "u2"), existingRestaurants = List.of("r1", "r2"))

automator.createOrder(orderId = "o4", restaurantId = "r2", userId = "u2", deliveryETA = 400, currentTime = 300)

Method Call 1

automator.updateOrderStatus(orderId = "o4", by = "DASHER", currentTime = 408, statusCode = "ORDER_DELIVERED")

Output 1

List.of("ORDER:o4", "BY:DASHER", "STATUS:ORDER_DELIVERED", "LATE:true", "ACTION:COMPLETE_ORDER")

Method Call 2

automator.updateOrderStatus(orderId = "o4", by = "CUSTOMER", currentTime = 415, statusCode = "WANT_REFUND")

Output 2

List.of("ORDER:o4", "BY:CUSTOMER", "STATUS:WANT_REFUND", "ACTION:NO_REFUND_RULE_MATCH")

Example 5: Delivery cancellation gives 100% refund

Input

WorkflowAutomator automator = new WorkflowAutomator(existingUsers = List.of("u1", "u2"), existingRestaurants = List.of("r1", "r2"))

automator.createOrder(orderId = "o5", restaurantId = "r1", userId = "u1", deliveryETA = 500, currentTime = 450)

Method Call 1

automator.updateOrderStatus(orderId = "o5", by = "SUPPORT_AGENT", currentTime = 460, statusCode = "DELIVERY_CANCELLED")

Output 1

List.of("ORDER:o5", "BY:SUPPORT_AGENT", "STATUS:DELIVERY_CANCELLED", "ACTION:CANCEL_ORDER", "ACTION:ISSUE_REFUND", "REFUND_PERCENT:100")





Please use Laptop/Desktop or any other large screen to add/edit code.