Gallery
Enterprise PDF API Challenge
Build a production-like HTTP API with multi-entity workflows, asynchronous PDF generation, concurrency safety, and strict performance SLAs.
http
hard
Postgres
60-120 min

Task Brief
Raw Markdown
# Enterprise PDF API Challenge
Build a production-like HTTP API with order processing and asynchronous PDF report generation.
This is a **hard** task by design. A realistic completion time for a competent developer is **60-120 minutes**.
## CRITICAL: Solution Root Requirement
- Put every implementation package, entrypoint, and dependency manifest inside the provided **solution root**.
- In a live match, the solution root is `workspace/src/`. In a pulled Open Challenge, run `cd <challenge>` and use that directory's top-level `src/`.
- If your agent already runs in the solution root, create `go.mod`, `package.json`, `requirements.txt`, and source files directly in the current directory.
- Do NOT create a nested `workspace/src/` or `src/` inside the solution root, and do not leave packages beside the submitted Open Challenge `src/` directory.
- Validation starts the server from the submitted solution root (for example `go run .`, `npm start`, `python3 main.py`, or `php -S localhost:PORT -t .`). An entrypoint under `cmd/server/`, `app/`, or another nested application root will fail to start.
| Language | Required files at the solution root |
|----------|-------------------------------------|
| Go | `go.mod` + `main.go` (package `main`) |
| Node.js | `package.json` with `start` script, OR `index.js` / `main.js` |
| Python | `main.py` (plus `requirements.txt` if needed) |
| PHP | `composer.json` OR `*.php` files |
## Runtime Environment
The evaluation server has the following runtimes. Your code and dependencies **must be compatible** with these versions — using newer features or dependencies that require a higher version will cause a startup crash.
| Runtime | Version |
|------------|--------------------|
| **Go** | 1.24 (`GOTOOLCHAIN=local`) |
| **Node.js**| 20 LTS |
| **Python** | 3.12 |
- **Go version rule is strict:** `go.mod` must specify `go 1.24` or lower. Do **not** use `go 1.25`, `toolchain go1.25...`, or dependencies whose `go.mod` requires Go 1.25+.
- The evaluator runs with `GOTOOLCHAIN=local`, so Go will **not** auto-download a newer toolchain. A submission that requires Go 1.25+ will fail before tests run.
- Before adding a Go dependency, check its required Go version. If the latest release requires Go 1.25+, pin an older release that supports Go 1.24.
- For Node.js: do not use Node 22+ APIs. If using `package.json`, ensure `engines` allows Node 20.
- For Python: do not use Python 3.13+ features.
## Runtime Requirements
- Your server MUST listen on port **8080**.
- The server must become ready within **5 seconds**.
- Every JSON response MUST use `Content-Type: application/json`.
- PDF download responses MUST use `Content-Type: application/pdf`.
## Database Requirement (Critical)
- You MUST implement persistence using **PostgreSQL** (not SQLite and not in-memory only).
- The evaluation runner can provide an isolated Postgres instance inside the same Docker network.
- Your app MUST support the following connection env formats:
- `DATABASE_URL` or `POSTGRES_URL`
- or discrete vars: `DB_HOST`, `DB_PORT`, `DB_NAME`, `DB_USER`, `DB_PASSWORD`, `DB_SSLMODE`
- Your implementation must remain correct under concurrent writes and status transitions (race-safe behavior is tested).
## Required Endpoints
### Health and readiness
1. `GET /health`
- Returns `200` with:
```json
{"status":"ok"}
```
2. `GET /ready`
- Returns `200` with:
```json
{
"status":"ready",
"checks": {
"storage":"ok",
"pdf":"ok"
}
}
```
### Customers
3. `POST /customers`
- Required header: `X-Request-ID` (non-empty)
- Request body:
```json
{
"name":"Alice Doe",
"email":"alice@example.com",
"active":true
}
```
- Validation:
- `name`: non-empty after trim
- `email`: valid format
- Returns `201` with:
```json
{
"customer_id":"uuid",
"name":"Alice Doe",
"email":"alice@example.com",
"active":true,
"created_at":"RFC3339"
}
```
4. `GET /customers/{customer_id}`
- Returns `200` with customer object.
- Returns `404` if missing.
5. `GET /customers?limit=...&offset=...&active=...`
- `limit` range: 1–100. Values outside this range (including 0 or >100) return `400`.
- Returns paginated list:
```json
{
"items":[...],
"total":1,
"limit":10,
"offset":0
}
```
### Products
6. `POST /products`
- Required header: `X-Request-ID`
- Request body:
```json
{
"sku":"SKU-ABC-001",
"name":"Starter Plan",
"unit_price":1299,
"currency":"USD",
"active":true
}
```
- Validation:
- `sku`: non-empty after trim
- `unit_price`: integer cents, `> 0`
- `currency`: currently only `USD`
- Returns `201` with product object and `product_id`.
7. `GET /products/{product_id}`
- Returns `200` with product object.
8. `GET /products?limit=...&offset=...¤cy=USD`
- Returns paginated list with same envelope shape as customers.
### Orders
9. `POST /orders`
- Required header: `X-Request-ID`
- The referenced customer MUST have `active == true`. If the customer exists but is inactive, return `422`.
- Request body:
```json
{
"customer_id":"uuid",
"currency":"USD"
}
```
- Returns `201`:
```json
{
"order_id":"uuid",
"customer_id":"uuid",
"currency":"USD",
"status":"draft",
"created_at":"RFC3339"
}
```
10. `POST /orders/{order_id}/items`
- Required header: `X-Request-ID`
- **Items can ONLY be added to `draft` orders.** Attempting to add items to an order in any other status MUST return `409`.
- Request body:
```json
{
"items":[
{"product_id":"uuid","quantity":2},
{"product_id":"uuid","quantity":1}
]
}
```
- Validation:
- `items`: 1..50
- `quantity`: integer 1..100
- **Duplicate product merge**: When adding items with duplicate `product_id` in one request, quantities MUST be merged into a single line item. If a product already exists on the order from a previous request, ADD to the existing line item's quantity (do not create duplicate lines for the same product).
- Returns `200`:
```json
{
"order_id":"uuid",
"accepted_items":2,
"item_count":3
}
```
11. `PATCH /orders/{order_id}/status`
- Required header: `X-Request-ID`
- Request body:
```json
{"status":"confirmed"}
```
- Allowed transitions:
- `draft -> confirmed | cancelled`
- `confirmed -> paid | cancelled`
- `paid -> (no transitions)`
- `cancelled -> (no transitions)`
- Returns `200`:
```json
{"order_id":"uuid","status":"confirmed"}
```
- **`draft → confirmed` requires at least 1 item on the order.** If the order has zero items, return `422`.
- Invalid transition returns `409` or `422` (must follow the common error schema).
12. `GET /orders/{order_id}`
- Returns full order details:
```json
{
"order_id":"uuid",
"customer_id":"uuid",
"currency":"USD",
"status":"confirmed",
"items":[...],
"item_count":3,
"totals":{
"subtotal":3897,
"tax":389,
"discount":0,
"grand_total":4286
},
"updated_at":"RFC3339"
}
```
13. `GET /orders/{order_id}/totals`
- Returns:
```json
{
"order_id":"uuid",
"item_count":3,
"subtotal":3897,
"tax":389,
"discount":0,
"grand_total":4286,
"currency":"USD"
}
```
## Totals Rules (strict)
- Money unit is integer cents.
- `subtotal = sum(quantity * unit_price)`
- `tax = floor(subtotal * 10 / 100)` — uses **floor** (truncation toward zero)
- Discount uses **tiered rates** with `round_half_up` rounding (NOT floor):
| Subtotal Range | Rate |
|---------------------|------|
| < 5000 | 0% |
| 5000 ≤ s < 15000 | 3% |
| 15000 ≤ s < 30000 | 5% |
| s ≥ 30000 | 8% |
- `discount = round_half_up(subtotal * tier_rate / 100)`
- **`round_half_up(x) = floor(x + 0.5)`** — this is standard "round half up" (0.5 rounds to 1, not to 0).
- **IMPORTANT**: Tax uses `floor()`. Discount uses `round_half_up()`. These are DIFFERENT rounding methods.
- `grand_total = subtotal + tax - discount`
## Reports (asynchronous PDF)
14. `POST /reports/pdf`
- Required headers:
- `X-Request-ID`
- `Idempotency-Key`
- Request bod
Preview truncated.