Let the front desk renew a membership for a month
Java · Spring Boot · beginner · modification
Members who pay cash at the front desk currently need a manager to edit their renewal date by hand. This adds `POST /api/front-desk/memberships/{id}/renewal`, which the till calls once a payment for one month has gone through. `MembershipRenewalService.renewForOneMonth` loads the membership with a row lock (a new `findLockedById` with `PESSIMISTIC_WRITE`), so two tills renewing the same member queue up instead of overwriting each other, extends `renewsOn` by a month and returns the updated view; the transaction commits when the method returns. An unknown id becomes a 404 through `ResponseStatusException`.
Gym management backend on Spring Boot 3.3 (Java 21), Spring Data JPA (Hibernate 6) on PostgreSQL, Spring Security. The existing, unchanged `SecurityConfig` lets only staff (`ROLE_STAFF`) call `/api/front-desk/**`. Existing and unchanged: the `Membership` entity (`id`, `memberId`, and `renewsOn`, a non-null `LocalDate` with a setter) and the `MembershipView` record with `MembershipView.from(membership)`. `renewsOn` is the first day the membership is no longer paid for: the door scanner admits a member while the club's local date is before `renewsOn`. Signup only accepts start dates on the 1st to the 28th of a month, so every `renewsOn` falls on day 1 to 28, and all dates lie between 2020 and 2100. The till sends exactly one renewal request per completed payment.
Requirements
- `POST /api/front-desk/memberships/{id}/renewal` records one month paid at the front desk: it moves the membership's `renewsOn` date forward by one calendar month and keeps the same day of the month (15 March becomes 15 April, 15 January becomes 15 February, 28 December becomes 28 January). It always extends from the current `renewsOn`, never from today, so paying a few days early or late makes no difference.
- On success it returns 200 with the saved `MembershipView` (id, renewsOn). An id with no membership returns 404.
- Two renewals of the same membership that arrive at the same time both count (two payments, two months); neither overwrites the other.
Files touched
- src/main/java/com/acme/gym/memberships/MembershipRepository.java
- src/main/java/com/acme/gym/memberships/MembershipRenewalController.java
- src/main/java/com/acme/gym/memberships/MembershipRenewalService.java
--- src/main/java/com/acme/gym/memberships/MembershipRepository.java
package com.acme.gym.memberships;
+import jakarta.persistence.LockModeType;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Lock;
+import java.util.Optional;
public interface MembershipRepository extends JpaRepository<Membership, Long> {
+
+ @Lock(LockModeType.PESSIMISTIC_WRITE)
+ Optional<Membership> findLockedById(Long id);
}
--- src/main/java/com/acme/gym/memberships/MembershipRenewalController.java
+package com.acme.gym.memberships;
+
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/front-desk/memberships")
+public class MembershipRenewalController {
+
+ private final MembershipRenewalService renewalService;
+
+ MembershipRenewalController(MembershipRenewalService renewalService) {
+ this.renewalService = renewalService;