Katkat

Merchant-Locked Payment Codes: Trade-offs and a Buggy Checker

Capital OneSoftware Engineer · Technical Screen

Last updated

A three-part fintech case on merchant-locked, single-merchant payment codes: discuss their trade-offs for cardholder and issuer, hand-validate sample codes against a digit-position rule table, then find and fix three planted bugs in a flawed validator.

A card issuer wants to add merchant-locked payment codes inside its mobile app. A cardholder spins up a 12-digit code against their real account, pins it to exactly one merchant, and uses it only there until it either lapses on its own or the cardholder kills it manually. The exercise has three linked stages: talking through the feature, walking through sample codes by hand, and fixing a broken piece of validation logic built from the same rules.

Code layout for this exercise

A code runs 12 digits long, positions 1 through 12 counted from the left. Here's what governs each segment:

DigitsWhat it representsPasses validation when
1Usage mode: 1 single-use, 2 ongoingvalue is 1 or 2
2-5Expiration written as MMYY, interpreted as 20YYMM falls between 01 and 12, and the transaction date doesn't fall after that month's final day
6Currency marker: 1 USD, 2 EUR, 3 GBPthe digit maps to a real currency and that currency matches what the transaction is actually in
7-115-digit merchant lockmatches the merchant the code was generated against
12Checksum digitequal to the sum of positions 1 through 11, taken mod 10

A code is only acceptable if every one of those five checks comes back clean.

Clarifying Questions

  • Does a human ever manually key in the 12 digits, or does it always move between systems programmatically?
  • Is a cardholder allowed more than one live code simultaneously, say one per subscription?
  • When walking through samples by hand, do you report just the first broken rule, or everything a code violates?
  • Should the sample payments be worked through in the order they're listed?

Part 1 — Trade-offs for cardholder and issuer

Walk through what a merchant-locked code is worth, and what it costs, on both sides of the relationship — the cardholder using it and the issuer running it — by tracing one code from creation through everyday purchases, a refund, and eventual cancellation.

Part 2 — Validate by hand

Using the table above, decide which of these codes are acceptable, assuming today's payment date is March 15, 2026:

  1. 107261402126 — claimed merchant ID 40212, currency USD
  2. 207260402137 — claimed merchant ID 40213, currency USD
  3. 113261402123 — claimed merchant ID 40212, currency USD
  4. 101261402120 — claimed merchant ID 40212, currency USD

For each, state which rule (if any) it fails, and show the check-digit arithmetic.

Part 3 — Debug a flawed validator

Below is a validator meant to implement the same five rules. It has three independent bugs — a wrong boundary comparison, an inverted condition, and an off-by-one in which digits get summed for the check digit. Find all three and explain, for each, a specific input that the buggy code gets wrong.

def validate_code(code, payment_date, merchant_id, currency):
    if len(code) != 12 or not code.isdigit():
        return False
    digits = [int(c) for c in code]

    if digits[0] not in (1, 2):
        return False

    mm = digits[1] * 10 + digits[2]
    yy = digits[3] * 10 + digits[4]
    if not (1 < mm < 12):
        return False
    year = 2000 + yy
    expiry_end = last_day_of_month(mm, year)
    if payment_date > expiry_end:
        return False

    currency_map = {1: "USD", 2: "EUR", 3: "GBP"}
    if digits[5] in currency_map and currency_map[digits[5]] == currency:
        return False

    locked_id = "".join(str(d) for d in digits[6:11])
    if locked_id != merchant_id:
        return False

    check = sum(digits[0:12]) % 10
    if digits[11] != check:
        return False

    return True

Hints

Hint 1

Work the digit table top to bottom for every sample code before deciding pass or fail — skipping a rule early is where hand-checks usually go wrong.

Hint 2

For the boundary bug, test the two edges of the month range directly: month 1 and month 12, not just a middle value.

Hint 3

For the inverted condition, ask what the code does when the currency digit correctly matches, versus when it doesn't — compare both paths to what the rule actually requires.

Hint 4

The check-digit slice should exclude the digit it's computing — check exactly which index range the buggy code sums.

Follow-up questions

  • How would you change the format to support a code usable at a small whitelist of merchants instead of exactly one?
  • What would you add to the rule set to prevent a canceled code from being replayed if an authorization was already queued before cancellation landed?
  • How would you test this validator to automatically catch all three bug classes, rather than relying on hand-picked inputs?
  • How would you version the rule table itself if the issuer later wants to add a fourth currency without breaking already-issued codes?