Response
Writing files isn't permitted in this session, but that's fine — the task only asked me to print the source, not create files. Here it is.
I'm assuming `ApiResponse` (response envelope) already exists in a shared package (`com.acme.common`), per house convention, and that `InvoiceService.create(...)` returns some existing `Invoice`-shaped type exposing `id()`, `customerId()`, `amountCents()`, `note()`, `createdAt()` — adjust the accessor names/import if the real type differs.
```java
package com.acme.billing;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import java.time.Instant;
public class InvoiceDtos {
private InvoiceDtos() {
}
public record CreateInvoiceRequest(
@Positive long customerId,
@Min(1) long amountCents,
@Size(max = 200) String note) {
}
public record InvoiceResponse(
long id,
long customerId,
long amountCents,
String note,
Instant createdAt) {
public static InvoiceResponse from(Invoice invoice) {
return new InvoiceResponse(
invoice.id(),
invoice.customerId(),
invoice.amountCents(),
invoice.note(),
invoice.createdAt());
}
}
}
```
```java
package com.acme.billing;
import com.acme.common.ApiResponse;
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 static com.acme.billing.InvoiceDtos.CreateInvoiceRequest;
import static com.acme.billing.InvoiceDtos.InvoiceResponse;
@RestController
@RequestMapping("/api/billing")
public class InvoiceController {
private final InvoiceService invoiceService;
public InvoiceController(InvoiceService invoiceService) {
this.invoiceService = invoiceService;
}
@PostMapping("/invoices")
public ResponseEntity<ApiResponse<InvoiceResponse>> create(@Valid @RequestBody CreateInvoiceRequest request) {
Invoice invoice = invoiceService.create(request.customerId(), request.amountCents(), request.note());
return ResponseEntity.status(HttpStatus.CREATED).body(ApiResponse.ok(InvoiceResponse.from(invoice)));
}
}
```
Conventions applied: constructor injection (no Lombok/field injection), request/response as records in an `InvoiceDtos` holder validated via `jakarta.validation` (`@Positive`, `@Min(1)`, `@Size(max=200)` — using primitive `long` means a missing field defaults to `0`, which these constraints already reject, so no separate `@NotNull` is needed), and a thin controller that validates, calls one service method, and wraps the result in `ApiResponse.ok(...)`.