Fix: voided/used coupons wrongly block re-adding the same barcode#273
Merged
Conversation
TryAddCoupon's primary duplicate-barcode check (DbService.fs ~line 231) had
no status filter, so a voided or used coupon with a future expires_at
permanently blocked re-adding the same barcode. This contradicts the
coupon_barcode_active_uniq partial-unique index (V18__coupon_reported_status.sql),
which only covers status IN ('available', 'taken', 'reported') — the schema
deliberately allows re-adding a barcode whose only prior row is voided or
used. The sibling 23505 race-recovery lookup a few lines below already had
the correct status filter (and a comment saying it must match the index
predicate); the primary check just never got it. User-visible effect: void
a coupon by mistake and you can't re-add it until its expiry passes.
Added the same status filter to the primary check. Left the check's
`expires_at >= @today` breadth as-is (vs. the index's exact-date match) —
that's a deliberate, separate guard against re-adding the same physical
coupon under a different (e.g. mistyped) future date, not a stand-in for
the index; a comment at the site now says so explicitly.
Added tests/CouponHubBot.Tests/DuplicateBarcodeStatusTests.fs (192 -> 198):
voided/used coupons no longer block re-adding; available/taken/reported
still do; and a same-barcode-different-future-date add still blocks,
pinning the expires_at >= @today decision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
Void a coupon by mistake, and you can't re-add another coupon with the same barcode until the voided coupon's
expires_atpasses — the user gets «Купон с таким штрихкодом уже есть в базе и ещё не истёк». Affects manual/add, the wizard'saddflow:confirmandaddflow:ocr:yes, and the bulk/album confirm — all route throughTryAddCoupon. Exposure grew with #269, which made manual/addextract barcodes (before that, manual-add coupons had anullbarcode and skipped this check entirely).Root cause
src/CouponHubBot/Services/DbService.fs'sTryAddCouponhas two duplicate-barcode checks that disagreed:WHERE barcode_text = @barcode_text AND expires_at >= @today— no status filter at all.WHERE barcode_text = @barcode_text AND expires_at = @expires_at AND status IN ('available', 'taken', 'reported')— has the correct filter, plus a comment saying the status list must match the unique index's predicate exactly.The partial unique index
coupon_barcode_active_uniq(V18__coupon_reported_status.sql) only coversstatus IN ('available', 'taken', 'reported'):The schema deliberately permits re-adding a barcode whose previous row is
voidedorused. The primary check ignored that and blocked it anyway — it just never got the status filter its sibling recovery path already had.Fix
Added
AND status IN ('available', 'taken', 'reported')to the primary check, matching the index predicate and the sibling recovery path.The
expires_atdivergence — decisionThere's a second, separate divergence: the primary check uses
expires_at >= @today(any future expiry) while the index (and the race-recovery path) key on exactexpires_atequality. I left this as-is. Reasoning: the migration comment that ties the recovery path's status list to the index predicate is scoped to that lookup only (it exists to identify which row a 23505 violation collided with, so it must match the index exactly). Nothing ties the primary check's date range to the index. I read the>=breadth as a deliberate, separate guard: it also catches the same barcode being re-added under a different (e.g. mistyped) future expiry date — a case the index alone would not block. Tightening it to exact-match would fix nothing this bug report is about and would change more behavior than requested, so I kept it and added a comment at the site making the intentional divergence explicit. Pinned by a new test (see below).Other status-blind
barcode_textqueriesGrepped every
barcode_textreference insrc/CouponHubBot. No other query has this pattern — the only two duplicate-barcode SELECTs are the pair above, and everything else (INSERT/UPDATEwrites, display code) isn't a status-blind duplicate check.Tests added (
tests/CouponHubBot.Tests/DuplicateBarcodeStatusTests.fs, 192 → 198)Voided coupon with future expiry does not block re-adding same barcodeUsed coupon with future expiry does not block re-adding same barcodeAvailable coupon with same barcode still blocks add— guard isn't guttedTaken coupon with same barcode still blocks addReported coupon with same barcode still blocks addActive coupon with a different future expires_at for same barcode still blocks add— pins theexpires_at >= @todaydecision aboveVerified these tests actually catch the regression: reverted the status filter locally, reran just this file — the
voided/usedtests failed as expected (add wrongly blocked), the other four still passed. Restored the fix and reran full suite green.No existing test encoded the old (buggy) behavior — checked
CouponTests.fs,BatchDuplicateTests.fs,OcrAddFlowTests.fs,VoidFlowTests.fs,BatchStateMachineTests.fs,BatchConcurrencyTests.fs; none re-add a barcode after voiding/using it, so none needed changes.Validation
dotnet build bots.slnx -c Release— 0 warnings, 0 errorsdotnet test tests/CouponHubBot.Tests -c Release— 198/198 passed (was 192/192 onmain)Scope
Only
TryAddCoupon's primary duplicate-barcode SQL was touched; no refactor of the surrounding method, no migration (the index was already correct), no changes totests/CouponHubBot.RealTests/(not run).Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU