Skip to content
echogeco
The harness in code · Code exercises

An approval-gate state machine

Implement the gate that decides each credit note once, refuses late, repeated and self-approved decisions, and turns silence into expired, never approved.

codehardExercise · 4 min · 45 of 75
On this page

A gate decides once. The four states are the easy part; the work is in what it refuses, and in a clock that turns silence into "expired" rather than "approved".

The situation

The 40-person wholesaler this module uses in place of the agency turns each damaged delivery to one of its 900 cafés into a credit note. Its automation drafts each one (the café, the lines, the amount) and posts it to you in chat with Approve and Reject buttons. Every credit note over $200 waits for a person; last quarter that was 140 requests.

Three went wrong. An approval tapped two days after the request issued a $380 credit to a café the bookkeeper had already refunded by phone. A double tap on Approve issued the same $260 credit twice. And the drafting agent, which had the approve tool in its tool list, approved its own $540 request.

A request now expires 4 hours after it is made, 14,400 seconds, and an expired request goes to the escalation queue, never to the accounting API. Approval gates decides where gates go and how long their clocks run; this exercise builds the one in why we write these five by hand.

The question

Implement the gate so that each of the three incidents is a refused event and the examples below hold. The read at now applies the clock too, so a request nobody touched reads as expired.

def solve(events, ttl, now):
    """events: [(t, item, action, actor)], t non-decreasing, in seconds. action is
    "request", "approve" or "reject"; actor names who sent it. ttl: seconds a request
    stays pending. now: when the states are read, at or after the last event.
    A request opens an unused item as "pending" with deadline t + ttl and records its
    actor as the requester. An approve or reject is accepted only while the item is
    pending, before its deadline, and from an actor other than the requester.
    Returns (refused, states): the indices of refused events, in order, and a dict
    mapping every opened item to "pending", "approved", "rejected" or "expired" at now."""
R = (0, "cn-1", "request", "agent")
solve([R, (600, "cn-1", "approve", "founder")], 14400, 1000)
    -> ([], {"cn-1": "approved"})
solve([R, (600, "cn-1", "approve", "founder"), (601, "cn-1", "approve", "founder")], 14400, 1000)
    -> ([2], {"cn-1": "approved"})        # the double tap
solve([R, (172800, "cn-1", "approve", "founder")], 14400, 172800)
    -> ([1], {"cn-1": "expired"})         # two days late
solve([R, (5, "cn-1", "approve", "agent")], 14400, 10)
    -> ([1], {"cn-1": "pending"})         # the agent approving itself
Only pending moves. A refused event is a loop that changes nothing, and the clock leads to expired, never to approved.
100%SVG (opens in a new tab)

Pinch to zoom · drag to pan

A strong answer · Rubric · Where answers go wrong · Solution

Write your answer first, then open