Skip to content

fix(orders): record discount usage against the table the code actually came from - #1717

Open
MOHITKOURAV01 wants to merge 1 commit into
AnthropicBots:mainfrom
MOHITKOURAV01:fix/1712-coupon-promo-table-confusion
Open

fix(orders): record discount usage against the table the code actually came from#1717
MOHITKOURAV01 wants to merge 1 commit into
AnthropicBots:mainfrom
MOHITKOURAV01:fix/1712-coupon-promo-table-confusion

Conversation

@MOHITKOURAV01

@MOHITKOURAV01 MOHITKOURAV01 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

What this fixes

Closes #1712

A discount code can resolve from either of two independent tables — coupons (ledger: coupon_usage) or promo_codes (ledger: promo_usage). Both use INT AUTO_INCREMENT primary keys starting at 1, so their id spaces overlap completely. The usage-tracking block in createOrderService() treated whatever id it was holding as a promo_codes id:

const appliedPromoId = appliedPromo ? appliedPromo.id : null;   // could be either table
...
if (appliedPromoId) {
    await connection.query("UPDATE promo_codes SET usage_count = usage_count + 1 WHERE id = ?", [appliedPromoId]).catch(() => {});
    if (user_id) {
        await connection.query("INSERT INTO promo_usage (promo_id, ...) VALUES (?, ...)", [appliedPromoId, ...]).catch(() => {});
    }
}

Redeeming coupon id 3 incremented promo id 3's usage_count and filed a promo_usage row for a promo nobody used. Both writes end in .catch(() => {}), so nothing surfaced anywhere.

The paired recordCouponUsage() call above it has the mirror-image problem — it runs UPDATE coupons SET used_count = used_count + 1 for a promo_codes redemption. So a single redemption wrote to three tables, two of them wrong.

couponService.validateCoupon() has always reported the answer — isPromoTable on the returned object — and the order flow was throwing it away at line 466.

What changed

New: backend/services/discountUsageService.js

The single place that decides which ledger a redemption belongs to:

  • resolveDiscountSource(promo, declaredSource) — trusts an explicitly declared source first, then isPromoTable, then infers from discount_type / discount_value (columns only promo rows carry). Returns null when there is genuinely nothing to go on.
  • recordDiscountUsage(connection, params) — routes to promo_codes + promo_usage, or delegates to couponService.recordCouponUsage() for coupons. Returns the source it wrote to.

backend/services/order.service.js

  • Carries appliedPromoSource alongside appliedPromo, set from couponVal.coupon.isPromoTable on the coupon path and to promo_codes on the legacy validatePromo() path.
  • The ~20-line tracking block is replaced by one recordDiscountUsage() call.

Deliberate choices

  • An unattributable code writes nothing. Guessing is worse than abstaining: writing to the wrong ledger silently burns another campaign's budget and leaves no signal. It logs a warning instead.
  • Failures log rather than vanish. The two .catch(() => {}) swallows are replaced with logger.error. Neither still fails the order — the order is already paid for — but a broken ledger write is now visible.
  • A guest still bumps the global counter, and still writes no per-account row. promo_usage.user_id needs an identity a guest does not have.
  • The routing lives in its own module rather than inline, because createOrderService is long enough that the branch could not be tested without standing up a full order.

Tests

New backend/tests/discountUsageService.test.js, 18 cases:

  • source resolution, including the "return null rather than guess" path
  • a coupons redemption never touching promo_codes or promo_usage, and vice-versa
  • the guest path, both failure paths, and the no-op guards
  • three assertions over order.service.js itself, pinning that the direct UPDATE promo_codes / INSERT INTO promo_usage statements are gone and that the source comes from the validator rather than from the id
Tests:       18 passed, 18 total

Ran the surrounding suites (order, checkout, promo, coupon, cart — 474 tests) with no new failures. cartBulkSelection.test.js has one failure that reproduces unchanged on main and is unrelated to this branch.

Scope

backend/services/order.service.js, one new service, one new test file. No schema, route or frontend changes.


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.

A discount code resolves from either the coupons table or the promo_codes
table. Both use INT AUTO_INCREMENT primary keys starting at 1, so their id
spaces overlap completely, but the usage-tracking block treated whatever id it
held as a promo_codes id:

    UPDATE promo_codes SET usage_count = usage_count + 1 WHERE id = ?
    INSERT INTO promo_usage (promo_id, ...) VALUES (?, ...)

Redeeming coupon id 3 therefore incremented promo id 3 and filed a promo_usage
row for a promo that was never used, consuming that campaign's budget and
tripping its per-user limits. Both writes ended in .catch(() => {}), so none of
it surfaced. The paired recordCouponUsage call had the mirror-image problem,
bumping coupons.used_count for a promo_codes redemption.

couponService already reports which table a code came from via isPromoTable;
the order flow was discarding it. It is now carried as appliedPromoSource and
the routing decision lives in a new discountUsageService, testable apart from
order creation. A code that cannot be attributed to a source is logged and left
alone rather than guessed at - writing to the wrong ledger is worse than not
writing. Both failure paths log instead of swallowing.

Closes AnthropicBots#1712
@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 redemptions increment the wrong table — a coupons.id is used to UPDATE promo_codes and INSERT INTO promo_usage

1 participant