You are building a payment reconciliation system that matches incoming payments to outstanding invoices. When customers send payments, you need to determine which invoice each payment is settling. The matching logic becomes progressively more sophisticated across three parts:
Payment (single string):
"payment-id, amount, memo"
payment-id: Unique identifier for the paymentamount: Payment amount (integer, in cents)memo: Payment description, may contain invoice referenceInvoices (list of strings): [Source: darkinterview.com]
[
"invoice-id, due-date, amount",
"invoice-id, due-date, amount",
...
]
invoice-id: Unique identifier for the invoicedue-date: Invoice due date in YYYY-MM-DD formatamount: Invoice amount (integer, in cents)Return a string describing the matched payment:
"Payment {payment-id} paid {amount} for invoice {invoice-id} due on {date}"
If no match is found, return:
"Payment {payment-id} could not be matched to any invoice"
Implement a function reconcile_payment(payment, invoices) that matches a payment to an invoice when the payment memo explicitly references an invoice ID. [Source: darkinterview.com]
The payment memo will contain text in the format "paying for: invoice-id" or "Paying off: invoice-id" (case-insensitive). Extract the invoice ID and find the matching invoice.
Input:
payment = "payment-001, 1000, Paying off: inv-123"
invoices = [
"inv-123, 2024-03-15, 1000",
"inv-456, 2024-03-20, 1000",
"inv-789, 2024-02-10, 500"
]
Output: [Source: darkinterview.com]
"Payment payment-001 paid 1000 for invoice inv-123 due on 2024-03-15"
Extend your solution to handle payments that don't reference a specific invoice ID. When no invoice ID is found in the memo, match the payment to an invoice with the exact same amount.
If multiple invoices have the same amount, select the invoice with the earliest due date.
Your solution must still support ID-based matching from Part 1 - prioritize ID matching over amount matching. [Source: darkinterview.com]
Input:
payment = "payment-002, 500, Monthly subscription"
invoices = [
"inv-001, 2024-03-22, 1000",
"inv-002, 2024-02-05, 500",
"inv-003, 2024-03-01, 500",
"inv-004, 2024-01-15, 500"
]
Output:
"Payment payment-002 paid 500 for invoice inv-004 due on 2024-01-15"
Note: inv-004 is selected because it has the earliest due date among invoices with amount 500 [Source: darkinterview.com]
Bank transfers may incur fees or rounding differences. A customer might receive an invoice for 2 fee, resulting in only $98 reaching Stripe. You need to reconcile these near-matches.
Add a third matching strategy: fuzzy amount matching with a forgiveness threshold. If no exact amount match is found, search for invoices where the amount is within ±forgiveness of the payment amount. Select the earliest due date if multiple invoices qualify.
Matching Priority: [Source: darkinterview.com]
Input:
payment = "payment-003, 98, Customer payment"
invoices = [
"inv-100, 2024-03-15, 100",
"inv-200, 2024-02-20, 98",
"inv-300, 2024-01-10, 102"
]
forgiveness = 5
Output:
"Payment payment-003 paid 98 for invoice inv-200 due on 2024-02-20"
Note: inv-200 has exact amount match, so it's selected over inv-100 (which would match within forgiveness) [Source: darkinterview.com]
Another Example with Fuzzy Match:
payment = "payment-004, 95, Customer payment"
invoices = [
"inv-100, 2024-03-15, 100",
"inv-300, 2024-01-10, 97"
]
forgiveness = 5
Output:
"Payment payment-004 paid 95 for invoice inv-300 due on 2024-01-10"
Note: Both invoices are within ±5 of 95, so we select inv-300 (earliest due date) [Source: darkinterview.com]
forgiveness as an additional parameterWhen writing test cases, ensure:
Strategy:
Implementation Approach: [Source: darkinterview.com]
def reconcile_payment(payment, invoices):
# Parse payment
parts = payment.split(", ")
payment_id = parts[0]
payment_amount = int(parts[1])
memo = ", ".join(parts[2:]) # Memo may contain commas
# Try to extract invoice ID from memo (case-insensitive search)
invoice_id = None
memo_lower = memo.lower()
if "paying for:" in memo_lower:
# Find position in lowercase, extract from original to preserve case
pos = memo_lower.find("paying for:") + len("paying for:")
invoice_id = memo[pos:].strip()
elif "paying off:" in memo_lower:
pos = memo_lower.find("paying off:") + len("paying off:")
invoice_id = memo[pos:].strip()
# Find matching invoice
if invoice_id:
for invoice in invoices:
inv_parts = invoice.split(", ")
inv_id = inv_parts[0]
if inv_id == invoice_id:
inv_date = inv_parts[1]
inv_amount = inv_parts[2]
return f"Payment {payment_id} paid {payment_amount} for invoice {inv_id} due on {inv_date}"
return f"Payment {payment_id} could not be matched to any invoice"
Time Complexity: O(n) where n is the number of invoices (linear search)
Space Complexity: O(1) - only storing parsed values
Strategy: [Source: darkinterview.com]
Key Implementation Details:
def reconcile_payment(payment, invoices):
# Parse payment
parts = payment.split(", ")
payment_id = parts[0]
payment_amount = int(parts[1])
memo = ", ".join(parts[2:])
# Try ID-based matching first
invoice_id = extract_invoice_id_from_memo(memo)
if invoice_id:
match = find_invoice_by_id(invoice_id, invoices)
if match:
return format_result(payment_id, payment_amount, match)
# Try amount-based matching
best_match = None
earliest_date = None
for invoice in invoices:
inv_id, inv_date, inv_amount = parse_invoice(invoice)
if int(inv_amount) == payment_amount:
if earliest_date is None or inv_date < earliest_date:
earliest_date = inv_date
best_match = (inv_id, inv_date, inv_amount)
if best_match:
return format_result(payment_id, payment_amount, best_match)
return f"Payment {payment_id} could not be matched to any invoice"
def parse_invoice(invoice):
"""Parse invoice string into components."""
parts = invoice.split(", ")
return parts[0], parts[1], parts[2]
def format_result(payment_id, payment_amount, invoice_match):
"""Format the reconciliation result."""
inv_id, inv_date, inv_amount = invoice_match
Time Complexity: O(n) - single pass through invoices for amount matching
Date Comparison Note: YYYY-MM-DD format can be compared as strings because it's lexicographically sortable. [Source: darkinterview.com]
Strategy:
Implementation Structure:
def reconcile_payment(payment, invoices, forgiveness=0):
# Parse payment
payment_id, payment_amount, memo = parse_payment(payment)
# Tier 1: ID-based matching
invoice_id = extract_invoice_id_from_memo(memo)
if invoice_id:
match = find_invoice_by_id(invoice_id, invoices)
if match:
return format_result(payment_id, payment_amount, match)
# Tier 2: Exact amount matching
exact_match = find_best_amount_match(
invoices,
payment_amount,
exact_only=True
)
if exact_match:
return format_result(payment_id, payment_amount, exact_match)
# Tier 3: Fuzzy amount matching
if forgiveness > 0:
fuzzy_match = find_best_fuzzy_match(
invoices,
payment_amount,
forgiveness
)
if fuzzy_match:
return format_result(payment_id, payment_amount, fuzzy_match)
return f"Payment {payment_id} could not be matched to any invoice"
def find_best_fuzzy_match(invoices, payment_amount, forgiveness):
"""Find earliest invoice within forgiveness range."""
best_match = None
earliest_date = None
min_amount = payment_amount - forgiveness
max_amount = payment_amount + forgiveness
for invoice in invoices:
inv_id, inv_date, inv_amount = parse_invoice(invoice)
inv_amount_int = int(inv_amount)
# Check if within forgiveness range (but not exact match)
if (min_amount <= inv_amount_int <= max_amount
and inv_amount_int != payment_amount):
if earliest_date is inv_date < earliest_date:
earliest_date = inv_date
best_match = (inv_id, inv_date, inv_amount)
best_match
Time Complexity: O(n) - at most three passes through invoices (one per tier) [Source: darkinterview.com]
Critical Logic:
Question: How would you design test cases to ensure the matching priority system works correctly across all three parts?
Key Testing Principles: [Source: darkinterview.com]
Backward Compatibility: Each part's test cases must verify that previous parts still function correctly
Priority Verification: Test cases must clearly demonstrate the matching hierarchy:
Example Test Cases: [Source: darkinterview.com]
def test_id_match_takes_precedence_over_amount():
"""Verify ID matching ignores amount-based matching."""
payment = "pay-001, 500, Paying for: inv-100"
invoices = [
"inv-100, 2024-03-15, 1000", # ID matches but amount differs
"inv-200, 2024-02-10, 500" # Amount matches but no ID match
]
# Should match inv-100 via ID, not inv-200 via amount
result = reconcile_payment(payment, invoices)
assert "inv-100" in result
def test_exact_amount_takes_precedence_over_fuzzy():
"""Verify exact amount match beats fuzzy match."""
payment = "pay-002, 100, Payment"
invoices = [
"inv-100, 2024-03-15, 100", # Exact match
"inv-200, 2024-01-10, 98" # Fuzzy match (within forgiveness)
]
forgiveness = 5
# Should match inv-100 (exact), not inv-200 (fuzzy)
result = reconcile_payment(payment, invoices, forgiveness)
assert "inv-100" in result
def test_fuzzy_match_only_when_no_exact_match():
"""Verify fuzzy matching only activates when needed."""
payment = "pay-003, 97, Payment"
invoices = [
"inv-100, 2024-03-15, 100", # Within forgiveness range
"inv-200, 2024-02-10, 200" # Outside range
]
forgiveness = 5
# Should match inv-100 via fuzzy logic
result = reconcile_payment(payment, invoices, forgiveness)
assert "inv-100" in result
def test_earliest_date_selection_within_same_tier():
"""Verify earliest date selection applies within each matching tier."""
payment =
invoices = [
,
,
]
result = reconcile_payment(payment, invoices)
result
():
reconcile_payment(
,
[, ]
)
reconcile_payment(
,
[, ]
)
reconcile_payment(
,
[, ],
forgiveness=
)
Testing Strategy Summary: