Skip to content

fix(coupons): enforce start_date, deleted_at and per_user_limit in coupon validation - #1716

Open
MOHITKOURAV01 wants to merge 1 commit into
AnthropicBots:mainfrom
MOHITKOURAV01:fix/1711-coupon-lifecycle-validation
Open

fix(coupons): enforce start_date, deleted_at and per_user_limit in coupon validation#1716
MOHITKOURAV01 wants to merge 1 commit into
AnthropicBots:mainfrom
MOHITKOURAV01:fix/1711-coupon-lifecycle-validation

Conversation

@MOHITKOURAV01

@MOHITKOURAV01 MOHITKOURAV01 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What this fixes

Closes #1711

validateCoupon() looked at is_active, an end date, the global usage_limit and minimum_order_amount. Three columns the coupons table has carried since migrations/0001_baseline_schema.sql were never read at all:

Column Consequence of skipping it
start_date TIMESTAMP NOT NULL A campaign scheduled to open next month discounts orders today.
deleted_at DATETIME A soft-deleted coupon keeps validating and keeps discounting.
per_user_limit INT DEFAULT 1 A "one per customer" coupon is unlimited per customer.

The userId argument was accepted and documented as "ID of the calling user" and then never referenced anywhere in the function body — which is exactly why the per-account cap was unenforceable.

What changed

backend/services/couponService.js

  • Start date. Rejects with "Coupon code is not active yet" when start_date / starts_at / valid_from is still in the future. All three spellings are accepted because the coupons and promo_codes tables name the column differently.
  • Soft deletion. A non-null deleted_at is rejected as an invalid code. Deliberately reuses the "Invalid coupon code" wording rather than announcing that a coupon used to exist.
  • Per-account limit. When a userId is supplied, prior redemptions are counted and checked against per_user_limit (or usage_limit_per_user on a promo row). Counting reads coupon_usage for a coupons-table code and promo_usage for a promo_codes code, so the two ledgers stay separate.
  • The ledger is now written. coupon_usage has existed since the baseline schema and nothing in the codebase ever inserted into it — without a row there is nothing for the cap to count. recordCouponUsage() now files one. Its new options argument is optional, so the existing three-argument call in order.service.js keeps working with no change.

Deliberate choices

  • Unparseable dates do not reject. parseDate() returns null for junk rather than a date, so a malformed start_date cannot make a live coupon look "not yet active". Rejecting a valid coupon is the worse failure here.
  • A zero or negative per_user_limit means uncapped, not "nobody may use this". A mis-seeded row should not lock out every shopper.
  • A missing ledger table counts as zero redemptions. If coupon_usage has not been migrated yet, checkout still completes and the global usage_limit remains the backstop — the alternative is a hard checkout failure on a schema gap.
  • Guests skip the per-account check entirely. coupon_usage.user_id is NOT NULL and a guest has no stable identity to cap against, so no query is issued and no row is written.
  • A failed ledger write never fails a paid order — it is logged and swallowed.

Tests

New backend/tests/couponLifecycle.test.js, 21 cases across start-date handling, soft deletion, per-account limits (including the coupons-vs-promo table split and the guest path), ledger writes, backwards compatibility of the three-argument call, and date parsing. They run against a stub connection, so no MySQL is needed in CI.

Verified the guards are meaningful: with the service reverted and only the tests applied, 11 of the 21 fail. With the fix in place all 21 pass.

Tests:       21 passed, 21 total

Scope

backend/services/couponService.js and one new test file. No route, schema or frontend changes — the call sites keep their current signatures.


CI note

Syntax check is red on this branch, and on every open PR in the repo right now. The single failure is inherited from main:

❌ 1 of 653 JavaScript file(s) failed to parse:
  frontend/scripts/shop.js:2499
      Unexpected end of input

No file this PR touches is implicated — shop.js is untouched here. It is tracked as #1696 with a fix open in #1701. Because Syntax check gates the rest of the workflow, Backend tests and Server boots report skipping rather than running; both suites pass locally on this branch.

The red Vercel check is the repo-wide Authorization required to deploy (team bhuvanshs-projects) and is unrelated to this change.

…lidateCoupon

validateCoupon checked is_active, an end date, the global usage_limit and
minimum_order_amount, and nothing else. Three columns the coupons table has
carried since the baseline schema were never read:

- start_date, so a campaign scheduled for next month discounted today
- deleted_at, so a soft-deleted coupon kept working
- per_user_limit, so a one-per-customer coupon was unlimited in practice

The userId parameter was accepted and documented but never referenced.

Adds the two date/lifecycle guards and a per-account redemption check backed
by the coupon_usage ledger (promo_usage for promo_codes codes), which nothing
in the codebase had ever written to. recordCouponUsage now files that ledger
row; its options argument is optional, so existing three-argument callers are
unaffected.

Date parsing is deliberately lenient: an unparseable column yields null rather
than a boundary that has already passed, so a malformed row cannot reject a
live coupon. A missing ledger table is counted as zero redemptions rather than
failing checkout, leaving the global usage_limit as the backstop.

Closes AnthropicBots#1711
@hydra-maintainer

Copy link
Copy Markdown

🔍 Quality Gate Report

✅ All quality gates passed!

Status Check Details
Linked Issue PR description references a closing issue ✅

@hydra-maintainer

Copy link
Copy Markdown

🤖 AI Code Review

🔴 Score: 50/100 | comment

AI review unavailable at this time.


Automated AI review — a human maintainer will also review.

@vercel

vercel Bot commented Aug 27, 2026

Copy link
Copy Markdown

@MOHITKOURAV01 is attempting to deploy a commit to the Bhuvansh's projects Team on Vercel.

A member of the Team first needs to authorize it.

@hydra-maintainer

Copy link
Copy Markdown

💡 Suggested reviewers based on relevant file history: @Aditya8369, @Pcmhacker-hero

@hydra-maintainer

Copy link
Copy Markdown

🟡 PR Health Score: 55/100

This PR's health score is below the 75/100 threshold for a healthy label.

Status Signal
Test coverage
Linked issue
PR description (≥50 chars)
DCO sign-off
⬜ 0 approval(s) Approvals
Diff size < 400 lines

Improving these signals will help reviewers engage faster and raise your score. 💪

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Coupon validation ignores start_date, deleted_at and per_user_limit — scheduled, deleted and single-use coupons all redeem freely

1 participant