Skip to content

feat: discount coupons with usage limits, expiry and minimum cart value - #3

Open
bahriy-dev wants to merge 1 commit into
mainfrom
feat/discount-coupons
Open

bahriy-dev wants to merge 1 commit into
mainfrom
feat/discount-coupons

Conversation

@bahriy-dev

@bahriy-dev bahriy-dev commented Nov 9, 2026 •

Copy link
Copy Markdown
Owner

Stores run campaigns with codes like HOSGELDIN10; the API had no way to model them.

What changed

  • Coupons: percent (1–100) or fixed amount in kuruş, with optional minimum cart subtotal, usage limit and expiry
  • Admin: GET /coupons, POST /coupons, DELETE /coupons/:code (deactivate, past orders keep their data)
  • Customer: PUT /cart/coupon, DELETE /cart/coupon; the cart shows the applied coupon and discount
  • Checkout: coupon re-checked and usage counted inside the same transaction (guarded UPDATE … WHERE used_count < max_uses), so it cannot be over-used
  • Cancel: the coupon use is given back
  • Money: VAT is recalculated on the discounted amount; free-shipping threshold is checked after the discount
  • Schema: migration 2 adds coupons, cart_coupons, orders.discount, orders.coupon_code
  • Tests: 9 new (36 total)

Closes #2

Adds percent and fixed-amount coupons. Admins create, list and deactivate them;
customers apply one coupon per cart. Usage is counted inside the checkout
transaction and returned when an order is cancelled. VAT and the free-shipping
threshold are calculated on the discounted amount. Orders store the discount
and coupon code (migration 2). Adds 9 tests.

Closes #2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FYiLwGkrgfn7ftZh1WDFpq
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-09T07:14:39.055046Z fef3d10 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fef3d10da9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/coupons.ts
Comment on lines +33 to +34
const coupon = findCoupon(db, row.code);
return coupon && coupon.isActive ? coupon : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude expired and exhausted coupons from cart totals

If a coupon expires after being applied, or another customer consumes its last permitted use, appliedCoupon still returns it because only isActive is checked. The cart view passes this result directly to calculateTotals, so GET /cart continues advertising a discount and total that checkout rejects with a 409. Check expiry and remaining uses when calculating the cart view, and expose the unavailable coupon without presenting its discount as usable.

Useful? React with 👍 / 👎.

Comment thread src/pricing.ts
const discount = coupon ? couponDiscount(coupon, subtotal) : 0;
const net = subtotal - discount;
// A discount lowers the taxable amount, so the VAT inside it shrinks proportionally.
const vatTotal = subtotal === 0 ? 0 : Math.round((grossVat * net) / subtotal);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Calculate discounted VAT before rounding

When the original VAT requires rounding, scaling the already-rounded grossVat introduces a second rounding error instead of recalculating VAT on the discounted amount. For example, a single item priced at 3 kuruş with 20% VAT and a fixed 1-kuruş discount produces vatTotal: 1, although vatIncluded(2, 20) is 0. This incorrect value is persisted on the order. Allocate the discount to taxable lines and calculate VAT from their discounted amounts before rounding, rather than scaling the rounded original VAT.

Useful? React with 👍 / 👎.

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.

Support discount coupons at checkout

1 participant