Add the receipts API for the web app

Java · Spring Boot · intermediate · modification

Adds the receipts API behind the web app's new Receipts tab. List and detail go through owner-filtered repository queries, so a user only ever gets their own receipts, and download hands out a 5-minute presigned link so the file bytes never pass through our servers. Each issued link is logged for the audit trail. Tested locally: uploaded three receipts from the mobile app, listed them, opened one, downloaded it.

Expense-tracking SaaS on Spring Boot 3.3 (Java 21, Gradle with the Spring Boot plugin), Spring Data JPA on PostgreSQL. Spring Security requires authentication for every `/api/**` route; the principal is `com.acme.expenses.auth.AppUser`, whose `getId()` returns the user's `Long` id. The `Receipt` JPA entity already exists in this package (fields `Long id`, `Long ownerId`, `fileName`, `contentType`, `long sizeBytes` (non-null), `storageKey`, `uploadedAt` as `Instant`, with getters) and is written by the mobile upload flow. `ReceiptStorage` is an existing S3-backed bean in the same package with `URI presignedGetUrl(String storageKey, Duration ttl)`. Receipt ids are sequential and appear in the web app's URLs. Receipts are photos of invoices and card slips: names, addresses, partial card numbers.

Requirements

Files touched

--- src/main/java/com/acme/expenses/receipt/ReceiptRepository.java
 package com.acme.expenses.receipt;
+
+import java.util.List;
+import java.util.Optional;
 
 import org.springframework.data.jpa.repository.JpaRepository;
 
 public interface ReceiptRepository extends JpaRepository<Receipt, Long> {
+
+    List<Receipt> findAllByOwnerIdOrderByUploadedAtDescIdDesc(Long ownerId);
+
+    Optional<Receipt> findByIdAndOwnerId(Long id, Long ownerId);
 }
 
--- src/main/java/com/acme/expenses/receipt/ReceiptView.java
+package com.acme.expenses.receipt;
+
+import java.time.Instant;
+
+public record ReceiptView(long id, String fileName, String contentType, long sizeBytes, Instant uploadedAt) {
+
+    static ReceiptView from(Receipt receipt) {
+        return new ReceiptView(
+                receipt.getId(),
+                receipt.getFileName(),
+                receipt.getContentType(),
+                receipt.getSizeBytes(),
+                receipt.getUploadedAt());
+    }

Review this PR

Java practice