Response
I'll write the request/response DTOs as validated records and the controller. Since `InvoiceService.create` "returns the created invoice's data," I'm assuming it returns a domain `Invoice` object exposing `getId()/getCustomerId()/getAmountCents()/getNote()/getCreatedAt()` — the controller maps that into the response DTO.
Empty project — no existing conventions to match, so I'll go with standard Spring Boot practice (records, `jakarta.validation`, constructor injection).
```java
package com.acme.billing;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;
import jakarta.validation.constraints.Size;
public record CreateInvoiceRequest(
@NotNull
@Positive
Long customerId,
@NotNull
@Positive
Long amountCents,
@Size(max = 200)
String note
) {
}
```
```java
package com.acme.billing;
import java.time.Instant;
public record InvoiceResponse(
long id,
long customerId,
long amountCents,
String note,
Instant createdAt
) {
}
```
```java
package com.acme.billing;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.net.URI;
@RestController
@RequestMapping("/api/billing/invoices")
public class InvoiceController {
private final InvoiceService invoiceService;
public InvoiceController(InvoiceService invoiceService) {
this.invoiceService = invoiceService;
}
@PostMapping
public ResponseEntity<InvoiceResponse> createInvoice(@Valid @RequestBody CreateInvoiceRequest request) {
Invoice invoice = invoiceService.create(
request.customerId(),
request.amountCents(),
request.note()
);
InvoiceResponse response = new InvoiceResponse(
invoice.getId(),
invoice.getCustomerId(),
invoice.getAmountCents(),
invoice.getNote(),
invoice.getCreatedAt()
);
return ResponseEntity
.status(HttpStatus.CREATED)
.location(URI.create("/api/billing/invoices/" + invoice.getId()))
.body(response);
}
}
```
Notes on the design choices:
- **`customerId`/`amountCents` are `Long`, not `long`**, in the request record — a primitive `long` silently defaults to `0` when the JSON field is missing, which would defeat `@NotNull` and let a missing field slip through as `amountCents=0` rather than failing "required" validation. `@Positive` then rejects `0` and negatives, and since the field is required to be positive there's no separate "at least 1" constraint needed beyond it.
- `note` has no `@NotNull` since it's optional; `@Size(max = 200)` alone permits `null` or short strings and rejects only overlong ones.
- The controller assumes an `Invoice` domain type returned by `InvoiceService.create` exposing `getId()/getCustomerId()/getAmountCents()/getNote()/getCreatedAt()` — adjust the mapping if the actual return type differs.
- A global `@ExceptionHandler`/`@ControllerAdvice` for `MethodArgumentNotValidException` isn't included since none exists yet in this project; without one, validation failures will fall back to Spring Boot's default error response.