/code-review high 2회차 · Claude Code · Opus 5.5 · 클로드의 마지막 답 원문 · 목록

I found 9 issues in points.py and ran the five most serious ones on example data to confirm them. The worst is a birthday multiplier bug that compounds: with a 3/1 birthday, the second purchase that day earns 20% instead of 10%. The rate also stays doubled on later days, so a purchase on 3/2 still earns 20%.

The other three confirmed bugs that let customers gain points they shouldn't: - Using points can hit expired entries: the expired points get deducted, and the usable balance doesn't drop. - The daily cap can be bypassed: after spending points, a customer can earn another 5,000 the same day. - Negative usage isn't blocked: use(-500) adds 500 points.

I also confirmed an off-by-one in expiry: points disappear on day 365, one day earlier than the rule says.

The rest are not tested: - Customers born Feb 29 miss the 2x bonus in non-leap years. - 0-point entries keep piling up once the cap is reached or a purchase is small. - The rate is kept as instance state (self.rate) when it could be computed per purchase. - Every earn or use scans the full entry history.

The full list is in the findings report.

찾은 문제 목록 (ReportFindings 도구로 넘긴 원문, 9건)

#위치분류요약일어나는 상황
1points.py:35정확성(버그)Birthday doubling permanently mutates self.rate
`earn` doubles `self.rate` in place on a birthday and never resets it, so the 2x rate compounds on every birthday purchase and persists for all later non-birthday days.
Wallet with birthday 3/1: earn(10000, 3/1) → 1000 (10%), second earn(10000, 3/1) → 2000 (20%), earn(10000, 3/2) → 2000 (rate stays 0.2). Verified: final `w.rate == 0.2`. Customers keep getting 4x, 8x… forever. Use a local `rate = POINT_RATE * (2 if birthday else 1)` instead of instance state.
2points.py:54정확성(버그)use() deducts from expired entries
`use` checks against `balance(today)` (unexpired only) but then deducts oldest-first from all entries, including expired ones, so spending hits dead points and the valid balance doesn't drop.
Entries: 1000 earned 2025-01-01 (expired), 1000 earned 2026-06-01. On 2026-06-02 balance=1000; use(500) takes 500 from the expired entry, balance stays 1000 — customer spends 500 points for free (verified). Skip entries outside the expiry window in the loop.
3points.py:37정확성(버그)Daily cap counts remaining, not earned, points
`earned_today` sums `entry[1]`, which `use()` decrements, so the daily cap measures today's *remaining* points rather than points earned today.
earn(100000, d) → 5000 (capped); use(5000, d); earn(100000, d) → another 5000. Total earned that day 10000, double the 5,000 DAILY_CAP (verified). Track the originally earned amount separately from the remaining amount.
4points.py:51정확성(버그)use() accepts negative/zero points
`use` has no lower-bound validation; a negative `points` passes the balance check and `min(entry[1], left)` returns the negative value, increasing the entry.
Wallet with 1000 points: use(-500, d) → first entry becomes 1500, balance 1500 (verified). A bad caller input mints points. Reject `points <= 0`.
5points.py:45정확성(버그)Expiry is one day early (off-by-one)
`(today - d).days < EXPIRE_DAYS - 1` makes points unusable on the 365th day, contradicting the docstring rule (earn day = day 1, usable through day 365); `use()` now relies on this check.
Earn on 2026-01-01; on day 365 (diff 364 days, 2026-12-31) balance returns 0 and use() raises '포인트가 부족합니다' (verified). Condition should be `< EXPIRE_DAYS`.
6points.py:29정확성(버그)Feb 29 birthdays never get the 2x bonus
Birthday match requires exact (month, day), so customers born Feb 29 get no birthday bonus in non-leap years.
birthday=date(2000,2,29); in 2026/2027/2025 there is no 2/29, `_is_birthday` is never true — 3 of every 4 years the customer gets no 2x reward. Decide a fallback (e.g. Feb 28 or Mar 1 in non-leap years).
7points.py:39정확성(버그)Zero-point entries appended once cap is hit
When the daily cap is already reached, `points` becomes 0 but a `[today, 0]` entry is still appended (and small payments also produce 0-point entries).
A regular hitting the cap early then making 20 more purchases accumulates 20 empty entries that every `balance`, `use` sort, and `earned_today` scan must iterate forever. Skip appending when points == 0.
8points.py:23단순화self.rate is derivable state; compute locally
`self.rate` is mutable instance state that is fully derivable from POINT_RATE and the birthday check; storing it is what enables the compounding bug.
Any per-purchase multiplier added later (events, promos) will also mutate shared state and leak across calls. Remove the attribute and compute `rate` inside `earn`.
9points.py:37효율earned_today scans full history every earn
Each `earn` scans every entry ever recorded to sum today's points, and `use` re-sorts all entries plus calls `balance` (another full scan) each time.
Long-time customers accumulate thousands of entries (one per purchase, never pruned); every purchase/use becomes O(n) over the whole history. Entries are appended chronologically, so iterate from the end for today's sum and skip sorting, or prune expired/empty entries.