Response
```java
package com.acme.billing;
import java.util.List;
public record ApiResponse<T>(boolean success, T data, PageMeta page, String error) {
public static <T> ApiResponse<T> ok(T data) {
return new ApiResponse<>(true, data, null, null);
}
public static <T> ApiResponse<List<T>> ok(List<T> items, PageMeta page) {
return new ApiResponse<>(true, items, page, null);
}
public static <T> ApiResponse<T> error(String message) {
return new ApiResponse<>(false, null, null, message);
}
}
```
```java
package com.acme.billing;
public record PageMeta(int page, int size, long total) {
public static PageMeta of(int page, int size, long total) {
return new PageMeta(page, size, total);
}
}
```
```java
package com.acme.billing;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/billing")
public class InvoiceController {
private static final int MAX_PAGE_SIZE = 100;
private final InvoiceService invoiceService;
public InvoiceController(InvoiceService invoiceService) {
this.invoiceService = invoiceService;
}
@GetMapping("/invoices")
public ApiResponse<?> listInvoices(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size) {
int pageNo = Math.max(page, 1);
int pageSize = Math.clamp(size, 1, MAX_PAGE_SIZE);
InvoiceQueryResult result = invoiceService.listInvoices(pageNo, pageSize);
return ApiResponse.ok(result.items(), PageMeta.of(pageNo, pageSize, result.total()));
}
}
```
Convention decisions applied: constructor injection (no Lombok/`@Autowired`), 1-based page/size clamped to `MAX_PAGE_SIZE = 100`, and the list wrapped via `ApiResponse.ok(items, PageMeta.of(...))` rather than returning `result` or a bare list directly.
Note: `ApiResponse`/`PageMeta` aren't defined in your prompt, so I included minimal versions in `com.acme.billing` per the skill's wrapper convention — swap them for your shared versions if they already exist elsewhere.