Compute Available Offers per User
Company: Affirm
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: easy
Interview Round: Technical Screen
Quick Answer: This question evaluates event-stream processing, stateful per-user counting, and time-window filtering competencies, requiring correctness with ordered inputs and enforcement of per-offer redemption constraints.
Constraints
- 1 <= len(offers) <= 10^4
- 0 <= number of event rows in events_csv <= 2 * 10^5
- 0 <= user_max_redemption_count <= 10^9
- Offer names are unique
- All timestamps are valid ISO-8601 strings without timezones
- When non-empty, events_csv contains the header: user_id,offer_name,event_time
Examples
Input: ([('Offer_1', '2024-01-01T00:00:00', '2024-01-31T23:59:59', 2), ('Offer_2', '2024-01-10T00:00:00', '2024-01-20T23:59:59', 1), ('Offer_3', '2024-02-01T00:00:00', '2024-02-10T23:59:59', 5)], 'user_id,offer_name,event_time\nu1,Offer_1,2024-01-05T10:00:00\nu1,Offer_1,2024-01-06T10:00:00\nu1,Offer_1,2024-01-07T10:00:00\nu1,Offer_2,2024-01-15T12:00:00\nu2,Offer_1,2024-01-08T09:00:00\nu2,Missing,2024-01-08T09:05:00\nu3,Offer_2,2024-01-09T09:00:00', '2024-01-15T00:00:00')
Expected Output: {'u1': [], 'u2': ['Offer_1', 'Offer_2'], 'u3': ['Offer_1', 'Offer_2']}
Explanation: u1 has exhausted both active offers. u2 has one counted redemption on Offer_1 and none on Offer_2, so both are still available. u3 appears in the CSV via an invalid early event, but still gets all currently active offers with remaining capacity.
Input: ([('B', '2024-03-01T00:00:00', '2024-03-10T00:00:00', 1), ('A', '2024-03-05T00:00:00', '2024-03-20T00:00:00', 2)], 'user_id,offer_name,event_time\nu1,B,2024-03-01T00:00:00\nu1,A,2024-03-05T00:00:00\nu1,A,2024-03-20T00:00:00\nu2,B,2024-02-29T23:59:59', '2024-03-10T00:00:00')
Expected Output: {'u1': [], 'u2': ['A', 'B']}
Explanation: This checks inclusive boundaries. u1 redeems B exactly at its start and A exactly at both its start/end bounds, reaching both limits. u2's attempt on B is before the offer starts, so it does not count; both active offers remain available to u2, sorted lexicographically.
Input: ([('X', '2024-06-01T00:00:00', '2024-06-30T23:59:59', 1)], 'user_id,offer_name,event_time\n', '2024-06-15T00:00:00')
Expected Output: {}
Explanation: There are no event rows, so no users appear in the input and the result is an empty mapping.
Input: ([('Freebie', '2024-05-01T00:00:00', '2024-05-31T23:59:59', 0), ('Spring', '2024-05-01T00:00:00', '2024-05-31T23:59:59', 1), ('Expired', '2024-04-01T00:00:00', '2024-04-30T23:59:59', 10)], 'user_id,offer_name,event_time\nu1,Freebie,2024-05-10T10:00:00\nu1,Expired,2024-04-15T10:00:00\nu2,Unknown,2024-05-10T10:00:00', '2024-05-20T00:00:00')
Expected Output: {'u1': ['Spring'], 'u2': ['Spring']}
Explanation: Freebie has a per-user max of 0, so it is never available and its event never counts. Expired is not active on the processing date. Both users therefore only have Spring available.
Input: ([('Old', '2024-01-01T00:00:00', '2024-01-31T23:59:59', 5), ('Future', '2024-03-01T00:00:00', '2024-03-31T23:59:59', 2)], 'user_id,offer_name,event_time\nu1,Old,2024-01-10T10:00:00\nu2,Future,2024-02-28T10:00:00', '2024-02-15T00:00:00')
Expected Output: {'u1': [], 'u2': []}
Explanation: Old has already ended by the processing date and Future has not started yet, so no offers are active. Users who appear in the CSV must still be present in the output with empty lists.
Hints
- Use a hash map keyed by offer_name so you can validate an event and fetch its window and limit in O(1). Track redemption counts by (user_id, offer_name).
- When building the final answer, do not look only at offers a user has already tried. Any active offer with remaining capacity is still available to that user.