---
phase: 01-foundation-and-collection
plan: 02
type: execute
wave: 2
depends_on: ["01-01"]
files_modified:
  - src/server/index.ts
  - src/server/routes/items.ts
  - src/server/routes/categories.ts
  - src/server/routes/totals.ts
  - src/server/routes/images.ts
  - src/server/services/item.service.ts
  - src/server/services/category.service.ts
  - tests/services/item.service.test.ts
  - tests/services/category.service.test.ts
  - tests/services/totals.test.ts
  - tests/routes/items.test.ts
  - tests/routes/categories.test.ts
autonomous: true
requirements:
  - COLL-01
  - COLL-02
  - COLL-03
  - COLL-04

must_haves:
  truths:
    - "POST /api/items creates an item with name, weight, price, category, notes, and product link"
    - "PUT /api/items/:id updates any field on an existing item"
    - "DELETE /api/items/:id removes an item and cleans up its image file"
    - "POST /api/categories creates a category with name and emoji"
    - "PUT /api/categories/:id renames a category or changes its emoji"
    - "DELETE /api/categories/:id reassigns its items to Uncategorized then deletes the category"
    - "GET /api/totals returns per-category and global weight/cost/count aggregates"
    - "POST /api/images accepts a file upload and returns the filename"
  artifacts:
    - path: "src/server/services/item.service.ts"
      provides: "Item CRUD business logic"
      exports: ["getAllItems", "getItemById", "createItem", "updateItem", "deleteItem"]
    - path: "src/server/services/category.service.ts"
      provides: "Category CRUD with reassignment logic"
      exports: ["getAllCategories", "createCategory", "updateCategory", "deleteCategory"]
    - path: "src/server/routes/items.ts"
      provides: "Hono routes for /api/items"
    - path: "src/server/routes/categories.ts"
      provides: "Hono routes for /api/categories"
    - path: "src/server/routes/totals.ts"
      provides: "Hono route for /api/totals"
    - path: "src/server/routes/images.ts"
      provides: "Hono route for /api/images upload"
    - path: "tests/services/item.service.test.ts"
      provides: "Unit tests for item CRUD"
    - path: "tests/services/category.service.test.ts"
      provides: "Unit tests for category CRUD including reassignment"
    - path: "tests/services/totals.test.ts"
      provides: "Unit tests for totals aggregation"
  key_links:
    - from: "src/server/routes/items.ts"
      to: "src/server/services/item.service.ts"
      via: "Route handlers call service functions"
      pattern: "import.*item.service"
    - from: "src/server/services/item.service.ts"
      to: "src/db/schema.ts"
      via: "Drizzle queries against items table"
      pattern: "db\\..*from\\(items\\)"
    - from: "src/server/services/category.service.ts"
      to: "src/db/schema.ts"
      via: "Drizzle queries plus reassignment to Uncategorized on delete"
      pattern: "update.*items.*categoryId"
    - from: "src/server/routes/items.ts"
      to: "src/shared/schemas.ts"
      via: "Zod validation via @hono/zod-validator"
      pattern: "zValidator.*createItemSchema|updateItemSchema"
---

<objective>
Build the complete backend API: item CRUD, category CRUD with reassignment, computed totals, and image upload. Includes service layer with business logic and comprehensive tests.

Purpose: Provides the data layer and API endpoints that the frontend will consume. All four COLL requirements are addressed by the API.
Output: Working Hono API routes with validated inputs, service layer, and passing test suite.
</objective>

<execution_context>
@/home/jean-luc-makiola/.claude/get-shit-done/workflows/execute-plan.md
@/home/jean-luc-makiola/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation-and-collection/01-RESEARCH.md
@.planning/phases/01-foundation-and-collection/01-VALIDATION.md
@.planning/phases/01-foundation-and-collection/01-01-SUMMARY.md

<interfaces>
<!-- From Plan 01 artifacts needed by this plan -->

From src/db/schema.ts:
```typescript
export const categories = sqliteTable("categories", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull().unique(),
  emoji: text("emoji").notNull().default("..."),
  createdAt: integer("created_at", { mode: "timestamp" })...
});

export const items = sqliteTable("items", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull(),
  weightGrams: real("weight_grams"),
  priceCents: integer("price_cents"),
  categoryId: integer("category_id").notNull().references(() => categories.id),
  notes: text("notes"),
  productUrl: text("product_url"),
  imageFilename: text("image_filename"),
  createdAt: integer("created_at", { mode: "timestamp" })...,
  updatedAt: integer("updated_at", { mode: "timestamp" })...,
});
```

From src/shared/schemas.ts:
```typescript
export const createItemSchema = z.object({ name, weightGrams?, priceCents?, categoryId, notes?, productUrl? });
export const updateItemSchema = createItemSchema.partial().extend({ id });
export const createCategorySchema = z.object({ name, emoji? });
export const updateCategorySchema = z.object({ id, name?, emoji? });
```

From tests/helpers/db.ts:
```typescript
export function createTestDb(): DrizzleInstance;
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Service layer with tests for items, categories, and totals</name>
  <files>src/server/services/item.service.ts, src/server/services/category.service.ts, tests/services/item.service.test.ts, tests/services/category.service.test.ts, tests/services/totals.test.ts</files>
  <behavior>
    Item service tests:
    - createItem: creates item with all fields, returns item with id and timestamps
    - createItem: only name is required, other fields optional
    - getAllItems: returns all items with category info joined
    - getItemById: returns single item or null
    - updateItem: updates specified fields, sets updatedAt
    - deleteItem: removes item from DB, returns deleted item (for image cleanup)
    - deleteItem: returns null for non-existent id

    Category service tests:
    - createCategory: creates with name and emoji
    - createCategory: uses default emoji if not provided
    - getAllCategories: returns all categories
    - updateCategory: renames category
    - updateCategory: changes emoji
    - deleteCategory: reassigns items to Uncategorized (id=1) then deletes
    - deleteCategory: cannot delete Uncategorized (id=1)

    Totals tests:
    - getCategoryTotals: returns weight sum, cost sum, item count per category
    - getCategoryTotals: excludes empty categories (no items)
    - getGlobalTotals: returns overall weight, cost, count
    - getGlobalTotals: returns zeros when no items exist
  </behavior>
  <action>
    Write tests FIRST using createTestDb() from tests/helpers/db.ts. Each test gets a fresh in-memory DB.

    Then implement services:

    1. `src/server/services/item.service.ts`:
       - Functions accept a db instance parameter (for testability) with default to the production db
       - getAllItems(): SELECT items JOIN categories, returns items with category name and emoji
       - getItemById(id): SELECT single item or null
       - createItem(data: CreateItem): INSERT, return with id and timestamps
       - updateItem(id, data): UPDATE with updatedAt = new Date(), return updated item
       - deleteItem(id): SELECT item first (for image filename), DELETE, return the deleted item data

    2. `src/server/services/category.service.ts`:
       - getAllCategories(): SELECT all, ordered by name
       - createCategory(data: CreateCategory): INSERT, return with id
       - updateCategory(id, data): UPDATE name and/or emoji
       - deleteCategory(id): Guard against deleting id=1. UPDATE all items with this categoryId to categoryId=1, then DELETE the category. Use a transaction.

    3. Totals functions (can live in item.service.ts or a separate totals module):
       - getCategoryTotals(): Per RESEARCH.md Pattern 4 exactly. SELECT with SUM and COUNT, GROUP BY categoryId, JOIN categories.
       - getGlobalTotals(): SELECT SUM(weightGrams), SUM(priceCents), COUNT(*) from items.
  </action>
  <verify>
    <automated>bun test tests/services/ --bail</automated>
  </verify>
  <done>All service tests pass. Item CRUD, category CRUD with Uncategorized reassignment, and computed totals all work correctly against in-memory SQLite.</done>
</task>

<task type="auto">
  <name>Task 2: Hono API routes with validation, image upload, and integration tests</name>
  <files>src/server/routes/items.ts, src/server/routes/categories.ts, src/server/routes/totals.ts, src/server/routes/images.ts, src/server/index.ts, tests/routes/items.test.ts, tests/routes/categories.test.ts</files>
  <action>
    1. Create `src/server/routes/items.ts` per RESEARCH.md example:
       - GET / returns all items (calls getAllItems service)
       - GET /:id returns single item (404 if not found)
       - POST / validates body with zValidator("json", createItemSchema), calls createItem, returns 201
       - PUT /:id validates body with zValidator("json", updateItemSchema), calls updateItem, returns 200 or 404
       - DELETE /:id calls deleteItem, cleans up image file if item had imageFilename (try/catch, don't fail delete if file missing), returns 200 or 404
       - Export as itemRoutes

    2. Create `src/server/routes/categories.ts`:
       - GET / returns all categories
       - POST / validates with createCategorySchema, returns 201
       - PUT /:id validates with updateCategorySchema, returns 200 or 404
       - DELETE /:id calls deleteCategory, returns 200 or 400 (if trying to delete Uncategorized) or 404
       - Export as categoryRoutes

    3. Create `src/server/routes/totals.ts`:
       - GET / returns { categories: CategoryTotals[], global: GlobalTotals }
       - Export as totalRoutes

    4. Create `src/server/routes/images.ts`:
       - POST / accepts multipart/form-data with a single file field "image"
       - Validate: file exists, size under 5MB, type is image/jpeg, image/png, or image/webp
       - Generate unique filename: `${Date.now()}-${randomUUID()}.${extension}`
       - Write to uploads/ directory using Bun.write
       - Return 201 with { filename }
       - Export as imageRoutes

    5. Update `src/server/index.ts`:
       - Register all routes: app.route("/api/items", itemRoutes), app.route("/api/categories", categoryRoutes), app.route("/api/totals", totalRoutes), app.route("/api/images", imageRoutes)
       - Keep health check and static file serving from Plan 01

    6. Create integration tests `tests/routes/items.test.ts`:
       - Test POST /api/items with valid data returns 201
       - Test POST /api/items with missing name returns 400 (Zod validation)
       - Test GET /api/items returns array
       - Test PUT /api/items/:id updates fields
       - Test DELETE /api/items/:id returns success

    7. Create integration tests `tests/routes/categories.test.ts`:
       - Test POST /api/categories creates category
       - Test DELETE /api/categories/:id reassigns items
       - Test DELETE /api/categories/1 returns 400 (cannot delete Uncategorized)

    NOTE for integration tests: Use Hono's app.request() method for testing without starting a real server. Create a test app instance with an in-memory DB injected.
  </action>
  <verify>
    <automated>bun test --bail</automated>
  </verify>
  <done>All API routes respond correctly. Validation rejects invalid input with 400. Item CRUD returns proper status codes. Category delete reassigns items. Totals endpoint returns computed aggregates. Image upload stores files. All integration tests pass.</done>
</task>

</tasks>

<verification>
- `bun test` passes all service and route tests
- `curl -X POST http://localhost:3000/api/categories -H 'Content-Type: application/json' -d '{"name":"Shelter","emoji":"tent emoji"}'` returns 201
- `curl -X POST http://localhost:3000/api/items -H 'Content-Type: application/json' -d '{"name":"Tent","categoryId":2}'` returns 201
- `curl http://localhost:3000/api/totals` returns category and global totals
- `curl -X DELETE http://localhost:3000/api/categories/1` returns 400 (cannot delete Uncategorized)
</verification>

<success_criteria>
- All item CRUD operations work via API (create, read, update, delete)
- All category CRUD operations work via API including reassignment on delete
- Totals endpoint returns correct per-category and global aggregates
- Image upload endpoint accepts files and stores them in uploads/
- Zod validation rejects invalid input with 400 status
- All tests pass with bun test
</success_criteria>

<output>
After completion, create `.planning/phases/01-foundation-and-collection/01-02-SUMMARY.md`
</output>
