/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건)
| # | 위치 | 분류 | 요약 | 일어나는 상황 |
|---|---|---|---|---|
| 1 | points.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. |
| 2 | points.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. |
| 3 | points.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. |
| 4 | points.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`. |
| 5 | points.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`. |
| 6 | points.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). |
| 7 | points.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. |
| 8 | points.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`. |
| 9 | points.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. |