Skip to content

Fix: voided/used coupons wrongly block re-adding the same barcode#273

Merged
Szer merged 1 commit into
mainfrom
fix/duplicate-barcode-status-filter
Jul 26, 2026
Merged

Fix: voided/used coupons wrongly block re-adding the same barcode#273
Szer merged 1 commit into
mainfrom
fix/duplicate-barcode-status-filter

Conversation

@Szer

@Szer Szer commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Symptom

Void a coupon by mistake, and you can't re-add another coupon with the same barcode until the voided coupon's expires_at passes — the user gets «Купон с таким штрихкодом уже есть в базе и ещё не истёк». Affects manual /add, the wizard's addflow:confirm and addflow:ocr:yes, and the bulk/album confirm — all route through TryAddCoupon. Exposure grew with #269, which made manual /add extract barcodes (before that, manual-add coupons had a null barcode and skipped this check entirely).

Root cause

src/CouponHubBot/Services/DbService.fs's TryAddCoupon has two duplicate-barcode checks that disagreed:

  • Primary check (~line 231, the one that runs first and actually gates every add): WHERE barcode_text = @barcode_text AND expires_at >= @today — no status filter at all.
  • 23505 race-recovery path (~line 296, only reached on a concurrent-insert race): 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 covers status IN ('available', 'taken', 'reported'):

CREATE UNIQUE INDEX IF NOT EXISTS coupon_barcode_active_uniq
    ON public.coupon (barcode_text, expires_at)
    WHERE barcode_text IS NOT NULL
      AND status IN ('available', 'taken', 'reported');

The schema deliberately permits re-adding a barcode whose previous row is voided or used. 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_at divergence — decision

There'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 exact expires_at equality. 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_text queries

Grepped every barcode_text reference in src/CouponHubBot. No other query has this pattern — the only two duplicate-barcode SELECTs are the pair above, and everything else (INSERT/UPDATE writes, 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 barcode
  • Used coupon with future expiry does not block re-adding same barcode
  • Available coupon with same barcode still blocks add — guard isn't gutted
  • Taken coupon with same barcode still blocks add
  • Reported coupon with same barcode still blocks add
  • Active coupon with a different future expires_at for same barcode still blocks add — pins the expires_at >= @today decision above

Verified these tests actually catch the regression: reverted the status filter locally, reran just this file — the voided/used tests 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 errors
  • dotnet test tests/CouponHubBot.Tests -c Release — 198/198 passed (was 192/192 on main)

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 to tests/CouponHubBot.RealTests/ (not run).

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01P41roCWN5Fec7m69sSaunU

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
@Szer
Szer merged commit d397f8b into main Jul 26, 2026
4 checks passed
@Szer
Szer deleted the fix/duplicate-barcode-status-filter branch July 26, 2026 10:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant