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

Files touched

--- 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());

Review this PR

Java practice