Skip to content

fix: widen the order number and stop a cart quantity wrapping negative - #87

Merged
bgard68 merged 1 commit into
mainfrom
claude/order-number-entropy
Aug 25, 2026
Merged

fix: widen the order number and stop a cart quantity wrapping negative#87
bgard68 merged 1 commit into
mainfrom
claude/order-number-entropy

Conversation

@bgard68

@bgard68 bgard68 commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Two defects found reading the checkout path after the hardening pass.

Order numbers collided at commerce scale

The number is WW-{date}-{suffix}, and the suffix was six hex characters of the order's Guid — 24 bits, scoped to a single day.

order_number carries a unique index, so a collision was never a data leak. But it was an INSERT that violated the constraint and rolled the whole placement back: a customer meeting a hard failure at checkout.

Collisions arrive by the birthday bound, not when the space runs out, so this bites far earlier than 16.7 million:

Orders in one day Chance of ≥1 collision
1,000 ~3%
5,000 ~53%
10,000 ~95%

The suffix is now ten characters — 40 bits, which stays under a rounding error past a million orders a day. The cost is four characters on a number people read aloud.

Worth noting the failure was at least in the safe order: TryPlaceAsync runs before the payment is charged, so a collision cost the customer an error rather than money.

A test pins the width so it cannot be shortened again for tidiness.

Cart quantity wrapped instead of clamping

AddCartItemHandler summed the existing line and the requested amount in int arithmetic before clamping to available stock:

var desired = Math.Min((existing?.Quantity ?? 0) + command.Quantity, widget.QuantityAvailable);

A quantity near int.MaxValue wraps negative, Math.Min then picks the negative, and the shopper is told the item is out of stock — misleading rather than dangerous, since nothing negative reached the cart.

The sum is widened to long before the clamp, so the clamp does the clamping: five available means five in the cart. No new policy, no new limit — just arithmetic that can't wrap.

Verification

  • 482 backend tests pass locally against real PostgreSQL 16
  • dotnet format --verify-no-changes clean, build clean under -warnaserror

Generated by Claude Code

Two defects found reading the checkout path after the hardening pass.

Order numbers collided at commerce scale. The number is WW-{date}-{suffix} and
the suffix was six hex characters of the order's Guid - 24 bits, scoped to a
single day. order_number carries a unique index, so a collision was never a
data leak, but it was an INSERT that violated the constraint and rolled the
whole placement back: a customer meeting a hard failure at checkout.

Collisions arrive by the birthday bound, not when the space runs out, so this
bites far earlier than 16.7 million. At a thousand orders in a day the chance of
at least one collision is around three per cent; at five thousand it is a coin
flip; at ten thousand it is near certain. The suffix is now ten characters - 40
bits - which stays under a rounding error past a million orders a day. The cost
is four characters on a number people read out.

The failure was at least in the safe order: TryPlaceAsync runs before the
payment is charged, so a collision cost the customer an error rather than money.

A test pins the width so it cannot be shortened again for tidiness.

Cart quantity wrapped instead of clamping. AddCartItemHandler summed the
existing line and the requested amount in int arithmetic before clamping to
available stock. A quantity near int.MaxValue wrapped negative, Math.Min then
chose the negative, and the shopper was told the item was out of stock - a
misleading answer rather than a dangerous one, since nothing negative reached
the cart. The sum is now widened to long before the clamp, so the clamp does
the clamping and five available means five in the cart.

482 backend tests pass locally against PostgreSQL 16; dotnet format clean.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EA4mmpcb1rcvNntHR1iG6j
@bgard68
bgard68 merged commit 7755ff0 into main Aug 25, 2026
8 checks passed
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.

2 participants