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.
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
A strong answer · Rubric · Where answers go wrong · Solution
Write your answer first, then open