Katkat

Tutor Billing Ledger With Idempotent Invoice Cutoffs

RipplingSoftware Engineer · Technical Screen

Last updated

Implement an in-memory ledger for hourly tutors that logs sessions, reports total and outstanding amounts in O(1), and invoices work up to a cutoff without ever billing the same minute twice. Tests stateful design, exact arithmetic, and handling of partial and late-logged work.

An online tutoring marketplace pays tutors by the hour for time spent in live sessions. Build an in-memory TutorLedger with:

class TutorLedger:
    def add_tutor(self, tutor_id: str, cents_per_hour: int) -> None: ...
    def log_session(self, tutor_id: str, start: int, end: int) -> None: ...
    def invoice_through(self, t: int): ...   # returns amount newly invoiced
    def total_billed(self): ...              # all logged work, invoiced or not
    def outstanding(self): ...               # logged work not yet invoiced
  • Times are integer seconds. A session's value is rate * (end - start) / 3600.
  • invoice_through(t) settles all logged work that happened at or before time t. If a session straddles t, the portion before t is settled now and the rest later.
  • Calling invoice_through again with the same or an earlier t must never settle anything twice.
  • A session may be logged after an invoice whose cutoff is later than the session's start ("late entry"). That work must not be lost: it is settled by the next invoice_through call, whatever its argument.
  • The two getters are called far more often than anything else and must be O(1).
  • Amounts must be exact; round to whole cents only for display.

Example

add_tutor("ana", 4500)          # $45.00/h
add_tutor("ben", 3000)          # $30.00/h
log_session("ana", 0, 3600)     # $45.00
log_session("ben", 1800, 9000)  # $60.00
total_billed(), outstanding()   -> $105.00, $105.00
invoice_through(5400)           -> $75.00   (ana 1h + ben 1h)
outstanding()                   -> $30.00
invoice_through(5400)           -> $0.00
invoice_through(3000)           -> $0.00
log_session("ana", 3600, 4000)  # late entry, $5.00
total_billed(), outstanding()   -> $110.00, $35.00
invoice_through(100)            -> $5.00

Constraints

  • Up to 10^5 tutors and 10^6 sessions. Single process, single thread.
  • 0 <= start <= end, rates are non-negative integers in cents per hour.
  • Unknown tutor IDs and end < start should raise.

Hints

Hint 1

Keep running totals for 'everything logged' and 'everything invoiced'; the getters are then a subtraction.

Hint 2

The effective cutoff is the maximum cutoff ever seen, not the latest argument. Store it as a watermark.

Hint 3

For each session, remember how far into it you have already paid. Fully paid sessions can be dropped from the working set.

Hint 4

rate_cents * seconds is an exact integer. Divide by 3600 only when reporting.

Follow-up questions

  • How would you support per-tutor outstanding balances and per-tutor invoices efficiently?
  • Rates change over time with effective dates. How does the data model change?
  • Make it safe for concurrent `log_session` and `invoice_through` calls from multiple threads.
  • If this were backed by a database, how would you make `invoice_through` idempotent across process crashes?