Earn loyalty points on paid orders

Java · Spring Boot · beginner · modification

Customers now earn loyalty points when they pay. A new `LoyaltyPoints` component turns an order total into points at the 5% earn rate, and `OrderPaymentService.markPaid` records a `LoyaltyEntry` for the order in the same transaction as the status change, so the points and the PAID status commit together. Orders that come to zero points are skipped, and the existing `PENDING_PAYMENT` guard (under the order row lock) means a redelivered webhook does not award points twice.

Shop backend on Spring Boot 3.3 (Java 21), Spring Data JPA (Hibernate 6) on PostgreSQL. `Order.total` is a non-null `numeric(12,2)` (`BigDecimal` with scale 2) between 0.00 and 100000.00: what the customer paid, shipping and tax included, 0.00 for a fully discounted order. Every order has a non-null `customerId` (there is no guest checkout). Existing and unchanged: the `Order` entity with `markPaid(Instant)`, `OrderStatus`, `OrderRepository.findByIdForUpdate` (`@Lock(PESSIMISTIC_WRITE)`, a row write lock: `SELECT ... FOR NO KEY UPDATE` on PostgreSQL), `OrderNotFoundException` (the webhook handler answers it with 404), a `Clock` bean, and the `LoyaltyEntry` entity with `LoyaltyEntryRepository` on the `loyalty_entries` table, already used by the points-for-a-product-review feature (entries are insert-only; a customer's balance is the sum of their entries). The webhook handler verifies the provider's signature, then calls `markPaid`; it opens no transaction of its own.

Requirements

Files touched

--- src/main/java/com/acme/shop/loyalty/LoyaltyPoints.java
+package com.acme.shop.loyalty;
+
+import java.math.BigDecimal;
+import org.springframework.stereotype.Component;
+
+@Component
+public class LoyaltyPoints {
+
+    private static final BigDecimal EARN_RATE = new BigDecimal("0.05");
+
+    public int forOrderTotal(BigDecimal total) {
+        return total.multiply(EARN_RATE).intValue();
+    }
+}
+
--- src/main/java/com/acme/shop/orders/OrderPaymentService.java
 package com.acme.shop.orders;
 
+import com.acme.shop.loyalty.LoyaltyEntry;
+import com.acme.shop.loyalty.LoyaltyEntryRepository;
+import com.acme.shop.loyalty.LoyaltyPoints;
 import java.time.Clock;
 import org.springframework.stereotype.Service;
 
     private final OrderRepository orders;
+    private final LoyaltyEntryRepository loyaltyEntries;
+    private final LoyaltyPoints loyaltyPoints;
     private final Clock clock;
 

Review this PR

Java practice