Determine Redeemable Promotion Offers
Company: Affirm
Role: Software Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Quick Answer: This question evaluates proficiency in event-stream processing, stateful counting, timestamp filtering, and enforcing business constraints for limited-use promotions, testing skills in date/time handling, aggregation, and mapping of user-event data.
Constraints
- 0 <= len(offers) <= 10^5
- 0 <= len(events) <= 10^5
- All timestamps use the exact format `YYYY-MM-DD HH:MM:SS`
- Each `offer_id` and `user_id` is a string
- `max_redeems_per_user` is a non-negative integer
Examples
Input: ([{'offer_id': 'A', 'start_time': '2015-01-01 00:00:00', 'end_time': '2015-12-31 23:59:59', 'max_redeems_per_user': 2}, {'offer_id': 'B', 'start_time': '2015-06-01 00:00:00', 'end_time': '2016-06-01 00:00:00', 'max_redeems_per_user': 1}, {'offer_id': 'C', 'start_time': '2015-01-01 00:00:00', 'end_time': '2015-12-01 00:00:00', 'max_redeems_per_user': 5}], [{'user_id': 'u1', 'timestamp': '2015-01-10 10:00:00', 'action': 'redeem', 'offer_id': 'A'}, {'user_id': 'u1', 'timestamp': '2015-12-30 09:00:00', 'action': 'redeem', 'offer_id': 'B'}, {'user_id': 'u1', 'timestamp': '2015-12-31 12:00:00', 'action': 'unredeem', 'offer_id': 'B'}, {'user_id': 'u2', 'timestamp': '2015-11-01 10:00:00', 'action': 'redeem', 'offer_id': 'A'}, {'user_id': 'u2', 'timestamp': '2015-11-02 10:00:00', 'action': 'redeem', 'offer_id': 'A'}, {'user_id': 'u2', 'timestamp': '2015-11-05 10:00:00', 'action': 'unredeem', 'offer_id': 'A'}, {'user_id': 'u2', 'timestamp': '2015-06-01 10:00:00', 'action': 'redeem', 'offer_id': 'C'}, {'user_id': 'u3', 'timestamp': '2016-01-01 00:00:00', 'action': 'redeem', 'offer_id': 'A'}])
Expected Output: {'u1': ['A', 'B'], 'u2': ['A', 'B'], 'u3': ['A', 'B']}
Explanation: Only A and B are active at the cutoff. u1 has A count 1 and B count 0. u2 has A count 1. u3 appears in the event list, but their only event is after the cutoff, so their counts are 0 for all active offers.
Input: ([{'offer_id': 'X', 'start_time': '2015-01-01 00:00:00', 'end_time': '2015-12-31 23:59:59', 'max_redeems_per_user': 1}, {'offer_id': 'Y', 'start_time': '2015-12-31 23:59:59', 'end_time': '2016-01-31 00:00:00', 'max_redeems_per_user': 2}, {'offer_id': 'Z', 'start_time': '2016-01-01 00:00:00', 'end_time': '2016-12-31 23:59:59', 'max_redeems_per_user': 1}], [{'user_id': 'alice', 'timestamp': '2015-03-01 00:00:00', 'action': 'unredeem', 'offer_id': 'X'}, {'user_id': 'alice', 'timestamp': '2015-12-31 23:59:59', 'action': 'redeem', 'offer_id': 'Y'}, {'user_id': 'bob', 'timestamp': '2015-12-31 23:59:58', 'action': 'redeem', 'offer_id': 'X'}, {'user_id': 'bob', 'timestamp': '2015-12-31 23:59:59', 'action': 'unredeem', 'offer_id': 'X'}, {'user_id': 'charlie', 'timestamp': '2015-07-01 10:00:00', 'action': 'redeem', 'offer_id': 'X'}, {'user_id': 'charlie', 'timestamp': '2015-12-30 10:00:00', 'action': 'redeem', 'offer_id': 'Y'}])
Expected Output: {'alice': ['X', 'Y'], 'bob': ['X', 'Y'], 'charlie': ['Y']}
Explanation: X and Y are active at the cutoff. Alice's unredeem on X does not make the count negative. Bob redeems and then unreedeems X back to 0. Charlie has X count 1, which equals its max, so X is no longer redeemable for Charlie.
Input: ([{'offer_id': 'D', 'start_time': '2015-01-01 00:00:00', 'end_time': '2016-01-01 00:00:00', 'max_redeems_per_user': 1}, {'offer_id': 'E', 'start_time': '2015-01-01 00:00:00', 'end_time': '2015-12-31 23:59:59', 'max_redeems_per_user': 3}], [{'user_id': 'u1', 'timestamp': '2015-12-31 23:59:59', 'action': 'redeem', 'offer_id': 'D'}, {'user_id': 'u1', 'timestamp': '2016-01-01 00:00:00', 'action': 'unredeem', 'offer_id': 'D'}, {'user_id': 'u1', 'timestamp': '2015-01-01 00:00:00', 'action': 'redeem', 'offer_id': 'E'}, {'user_id': 'u1', 'timestamp': '2015-06-01 00:00:00', 'action': 'redeem', 'offer_id': 'E'}, {'user_id': 'u1', 'timestamp': '2015-12-01 00:00:00', 'action': 'redeem', 'offer_id': 'E'}, {'user_id': 'u2', 'timestamp': '2016-01-02 00:00:00', 'action': 'redeem', 'offer_id': 'D'}])
Expected Output: {'u1': [], 'u2': ['D', 'E']}
Explanation: The future unredeem for u1 does not count, so u1 still has D count 1 and E count 3, both at their per-user limits. u2 appears in the event list but has no effective events before the cutoff, so both active offers are still available.
Input: ([{'offer_id': 'ONLY', 'start_time': '2015-01-01 00:00:00', 'end_time': '2015-12-31 23:59:59', 'max_redeems_per_user': 1}], [])
Expected Output: {}
Explanation: No users appear in the event list, so the result is an empty mapping.
Input: ([], [{'user_id': 'solo', 'timestamp': '2015-01-01 00:00:00', 'action': 'redeem', 'offer_id': 'M'}, {'user_id': 'solo', 'timestamp': '2015-01-02 00:00:00', 'action': 'unredeem', 'offer_id': 'M'}, {'user_id': 'pair', 'timestamp': '2016-01-01 00:00:00', 'action': 'redeem', 'offer_id': 'N'}])
Expected Output: {'pair': [], 'solo': []}
Explanation: There are no offers at all, but both users appear in the event list, so each must map to an empty list.
Hints
- First identify which offers are active at the cutoff time. Inactive offers can never appear in the answer.
- Track active redemption counts with a hash map keyed by `(user_id, offer_id)` or with nested dictionaries. Be careful that `unredeem` cannot make a count negative.