Repair and Extend a Transaction Reward Processor
Bilt RewardsSoftware Engineer · Technical ScreenLast updated
Implement and debug a layered transaction reward processing engine that calculates category-based points, applies promotional multipliers, enforces monthly qualification thresholds, and maintains strict idempotency.
Problem Statement
Reward card platforms need robust calculation engines to attribute points to member accounts based on transaction metadata, promotional calendar rules, and activity minimums.
Implement the TransactionRewardProcessor class responsible for processing incoming transactions, calculating reward points, tracking monthly activity, and ensuring idempotent execution.
Business Rules
- Base Calculation: Points are awarded per whole dollar spent (
amount_cents // 100). Partial dollars earn zero points. - Category Multipliers:
"DINING": 3 points per dollar."TRAVEL": 2 points per dollar."RENT": 1 point per dollar, capped at a maximum of100,000points per transaction.- Any other category: 1 point per dollar.
- First-of-Month ("Rent Day") Bonus:
- If the transaction occurs on the 1st day of any calendar month (UTC), multipliers for all categories except
"RENT"are doubled (Dining: 6x, Travel: 4x, Other: 2x). Rent remains 1x and capped at100,000.
- If the transaction occurs on the 1st day of any calendar month (UTC), multipliers for all categories except
- Monthly Qualification Rule:
- Points earned in a calendar month (e.g.,
"2024-03") remain"PENDING"until the user reaches at least 5 distinct valid transactions within that same calendar month. - Once the user reaches 5 transactions in that month, all transactions for that month become
"QUALIFIED".
- Points earned in a calendar month (e.g.,
- Idempotency:
- If a transaction with an existing
transaction_idis re-submitted, return the original result with status"DUPLICATE"without incrementing transaction counts or altering points balances.
- If a transaction with an existing
Interface
class TransactionRewardProcessor:
def __init__(self):
pass
def process_transaction(self, transaction: dict) -> dict:
"""
Processes a transaction and returns a receipt.
"""
pass
def get_user_monthly_summary(self, user_id: str, month: str) -> dict:
"""
Returns summary stats for a given user and month (format: 'YYYY-MM').
"""
pass
Input and Output Formats
Input transaction structure:
{
"transaction_id": "txn_101",
"user_id": "usr_01",
"amount_cents": 12500,
"category": "DINING",
"timestamp": "2024-03-01T14:30:00Z" # ISO 8601 UTC
}
Output of process_transaction:
{
"transaction_id": "txn_101",
"user_id": "usr_01",
"points_earned": 750,
"month": "2024-03",
"status": "PENDING" # "PENDING", "QUALIFIED", or "DUPLICATE"
}
Output of get_user_monthly_summary:
{
"user_id": "usr_01",
"month": "2024-03",
"transaction_count": 5,
"is_qualified": True,
"total_points": 1250
}
Constraints
1 <= amount_cents <= 100_000_000($1,000,000.00 max)- Timestamps are valid ISO 8601 UTC strings (
YYYY-MM-DDTHH:MM:SSZ). categoryis a case-insensitive string.
Example
processor = TransactionRewardProcessor()
# Transaction 1: $125.00 Dining on March 1st (6x multiplier applied)
processor.process_transaction({
"transaction_id": "t1",
"user_id": "u1",
"amount_cents": 12500,
"category": "DINING",
"timestamp": "2024-03-01T10:00:00Z"
})
# Returns: {"transaction_id": "t1", "user_id": "u1", "points_earned": 750, "month": "2024-03", "status": "PENDING"}
# Re-submitting the same transaction
processor.process_transaction({
"transaction_id": "t1",
"user_id": "u1",
"amount_cents": 12500,
"category": "DINING",
"timestamp": "2024-03-01T10:00:00Z"
})
# Returns: {"transaction_id": "t1", "user_id": "u1", "points_earned": 750, "month": "2024-03", "status": "DUPLICATE"}
Hints
Hint 1
Normalize timestamp parsing using UTC dates to extract both the `YYYY-MM` month identifier and the day-of-month check for Rent Day.
Hint 2
Store transactions keyed by `transaction_id` for O(1) duplicate checks before updating user monthly aggregates.
Hint 3
When a user logs their 5th transaction in a month, previously stored pending transactions for that month must retroactively reflect a qualified status.
Follow-up questions
- How would you ensure thread-safety and atomicity if transactions for the same user arrive concurrently across multiple application workers?
- If transaction records were persisted in a relational database like PostgreSQL, write the query or explain the schema design needed to efficiently compute the monthly summary.
- How would you handle transaction reversals or refunds (e.g., negative `amount_cents`), especially if a refund drops the monthly count below 5 after points were already marked qualified?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.