Let customers spend a gift card at checkout
Java · Spring Boot · beginner · modification
Adds gift card redemption so customers can pay part of an order with a card. `GiftCard` gets a `debit` method; `GiftCardService.redeem` loads the card with the existing `findByCodeForUpdate` (a row write lock, `SELECT ... FOR NO KEY UPDATE` on PostgreSQL), refuses an amount above the balance with 409 and debits the card; `GiftCardController` exposes it as `POST /api/gift-cards/{code}/redemptions` with a validated `RedeemRequest`. The card is a managed entity inside the transaction, so the change is flushed on commit without an explicit `save`.
Shop backend on Spring Boot 3.3 (Java 21), Spring Data JPA (Hibernate 6) on PostgreSQL, Spring Security. Every `/api/**` request needs a signed-in session; CSRF is handled by the existing `SecurityConfig`. Gift cards are bearer instruments: any signed-in customer who holds a code may check and spend it (codes are 16 random uppercase letters and digits, and the gateway rate-limits `/api/gift-cards/**`). One currency (EUR). Existing and unchanged: `GiftCardRepository` with `findByCode` and `findByCodeForUpdate` (`@Lock(PESSIMISTIC_WRITE)`, already used by the admin top-up flow), the `GiftCardView` record and `GiftCardNotFoundException` (`@ResponseStatus(NOT_FOUND)`). `gift_cards.balance` is `numeric(12,2) not null` and never negative. Bean Validation (`spring-boot-starter-validation`) is already in use. Errors use Spring Boot's default error body. Checkout sends each redemption once and does not retry; duplicate-submit protection lives in checkout (existing). Refunding a redeemed amount for a cancelled order goes through the existing refund flow and is out of scope.
Requirements
- `POST /api/gift-cards/{code}/redemptions` with a JSON body `{"amount": ...}` spends that amount from the card. On success it returns 200 with the card code, the redeemed amount and the remaining balance, and the stored balance is lower by exactly the redeemed amount. Checkout calls it when the customer applies a card and records the redeemed amount on the order (existing, unchanged).
- `amount` is required, at least 0.01, with at most two decimal places and at most 10 digits before the point; anything else is 400. An amount larger than the card's current balance is refused with 409 and changes nothing; spending exactly the remaining balance is allowed and leaves 0.00. An unknown code is 404.
- A card can never be spent beyond its balance in total, across any number of redemptions, including two redemptions of the same card that arrive at the same time.
Files touched
- src/main/java/com/acme/shop/giftcards/GiftCard.java
- src/main/java/com/acme/shop/giftcards/GiftCardService.java
- src/main/java/com/acme/shop/giftcards/GiftCardController.java
--- src/main/java/com/acme/shop/giftcards/GiftCard.java
return balance;
}
+
+ public void debit(BigDecimal amount) {
+ balance.subtract(amount);
+ }
}
--- src/main/java/com/acme/shop/giftcards/GiftCardService.java
package com.acme.shop.giftcards;
+import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import java.math.BigDecimal;
@Service
return GiftCardView.from(card);
}
+
+ @Transactional
+ public Redemption redeem(String code, BigDecimal amount) {
+ GiftCard card = repository.findByCodeForUpdate(code).orElseThrow(GiftCardNotFoundException::new);
+ if (amount.compareTo(card.getBalance()) > 0) {
+ throw new InsufficientBalanceException();
+ }
+ card.debit(amount);
+ return new Redemption(card.getCode(), amount, card.getBalance());