Katkat

Implement Billing Coding Exercises

StripeSoftware Engineer · Onsite

Last updated

Implement a subscription proration and billing reconciliation engine that generates line items across plan changes and computes the outstanding balance against posted payments.

Problem Statement

In recurring billing systems, customers frequently upgrade, downgrade, or cancel plans mid-cycle. You need to implement an invoice generation and reconciliation function that calculates prorated charges for a customer across a billing cycle and reconciles them with any payments made.

Each billing cycle is defined by an inclusive start date and an exclusive end date ([cycle_start, cycle_end)). Each plan has a daily rate expressed in integer cents. Throughout the cycle, the customer can transition between plans or cancel their subscription (plan_id = None).

Implement the function generate_billing_statement:

def generate_billing_statement(
    cycle_start: str,
    cycle_end: str,
    initial_plan_id: str | None,
    plan_changes: list[dict],
    payments: list[dict],
    plans: dict[str, int]
) -> dict:

Input

  • cycle_start, cycle_end: Strings in 'YYYY-MM-DD' format, where cycle_start < cycle_end.
  • initial_plan_id: The plan ID active at the start of cycle_start, or None if the customer had no active plan.
  • plan_changes: A list of objects with keys "date" (str, 'YYYY-MM-DD') and "plan_id" (str or None), representing the new plan effective from that date. Note: multiple changes can occur on the same day; only the last change for that date takes effect. Changes may arrive out of order.
  • payments: A list of objects with keys "id" (str), "date" (str), "amount_cents" (int), and "status" ("succeeded" or "failed").
  • plans: A dictionary mapping plan_id to daily_rate_cents (int > 0).

Output

Return a dictionary with the following schema:

  • "line_items": A list of active billing intervals chronologically ordered. Do not include intervals where plan_id is None or duration is 0 days. Each item contains:
    • "plan_id": str
    • "start_date": str ('YYYY-MM-DD')
    • "end_date": str ('YYYY-MM-DD')
    • "days": int
    • "amount_cents": int (days * daily_rate_cents)
  • "total_billed_cents": int (sum of amount_cents across line items)
  • "total_paid_cents": int (sum of amount_cents for all "succeeded" payments)
  • "amount_due_cents": int (total_billed_cents - total_paid_cents)

Constraints

  • All dates are valid ISO-8601 strings between "2000-01-01" and "2099-12-31".
  • All dates in plan_changes fall within [cycle_start, cycle_end).
  • 0 <= len(plan_changes) <= 10^4
  • 0 <= len(payments) <= 10^4

Example

cycle_start = "2024-03-01"
cycle_end = "2024-03-31"
initial_plan_id = "starter"
plan_changes = [
    {"date": "2024-03-11", "plan_id": "pro"},
    {"date": "2024-03-25", "plan_id": None}
]
payments = [
    {"id": "pay_1", "date": "2024-03-01", "amount_cents": 1500, "status": "succeeded"},
    {"id": "pay_2", "date": "2024-03-20", "amount_cents": 2000, "status": "failed"}
]
plans = {
    "starter": 100,  # 100 cents/day
    "pro": 250        # 250 cents/day
}

# Output:
# {
#   "line_items": [
#     {"plan_id": "starter", "start_date": "2024-03-01", "end_date": "2024-03-11", "days": 10, "amount_cents": 1000},
#     {"plan_id": "pro", "start_date": "2024-03-11", "end_date": "2024-03-25", "days": 14, "amount_cents": 3500}
#   ],
#   "total_billed_cents": 4500,
#   "total_paid_cents": 1500,
#   "amount_due_cents": 3000
# }

Hints

Hint 1

Deduplicate same-day plan changes by keeping the last change, and sort the unique transition dates in ascending order.

Hint 2

Form intervals between consecutive event dates, starting at cycle_start and ending at cycle_end, skipping zero-day intervals.

Hint 3

Filter payments strictly by status == 'succeeded' when summing total_paid_cents, and omit line items where the active plan is None.

Follow-up questions

  • How would you handle timezone differences if events include UTC timestamps while the billing cycle is defined in the merchant's local timezone?
  • How would you adapt the proration logic if plans are billed monthly (e.g., $30/month) rather than with pre-defined daily rates, accounting for months with 28, 30, or 31 days?
  • If multiple payments can be applied to specific line items (e.g., restricted credits or refunds), how would you track unpaid balances per line item?