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
- `GET /api/receipts` returns the signed-in user's receipts as metadata (`id`, `fileName`, `contentType`, `sizeBytes`, `uploadedAt`), newest upload first, ties broken by the higher id first. No pagination in this PR: receipts are archived monthly and a user has at most a few dozen active ones.
- `GET /api/receipts/{id}` returns the same metadata for one receipt.
- `GET /api/receipts/{id}/download` returns `{ "url", "expiresInSeconds" }`: a presigned storage URL valid for 5 minutes, which the web app opens to fetch the file. The response has no separate storage-key field.
- Every endpoint requires a signed-in user (already enforced for `/api/**` by `SecurityConfig`). A user may only see and download their own receipts: for another user's receipt id, both `/{id}` and `/{id}/download` answer 404, as if it did not exist.
- Read-only: uploads and deletes stay in the existing mobile flow and are out of scope.
Files touched
- src/main/java/com/acme/expenses/receipt/ReceiptRepository.java
- src/main/java/com/acme/expenses/receipt/ReceiptView.java
- src/main/java/com/acme/expenses/receipt/ReceiptController.java
--- 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());
+ }