Katkat

Repair and Extend a Transaction Reward Processor

Bilt RewardsSoftware Engineer · Technical Screen

Last 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

  1. Base Calculation: Points are awarded per whole dollar spent (amount_cents // 100). Partial dollars earn zero points.
  2. Category Multipliers:
    • "DINING": 3 points per dollar.
    • "TRAVEL": 2 points per dollar.
    • "RENT": 1 point per dollar, capped at a maximum of 100,000 points per transaction.
    • Any other category: 1 point per dollar.
  3. 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 at 100,000.
  4. 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".
  5. Idempotency:
    • If a transaction with an existing transaction_id is re-submitted, return the original result with status "DUPLICATE" without incrementing transaction counts or altering points balances.

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).
  • category is 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?