# Route Spec

## Route ID
`uploads-complete`

## Endpoint
`POST /api/v1/uploads/complete`

## Human Description
Finalizes a previously presigned upload after the client successfully PUTs the file to GCS. Validates object existence and returns a reusable file asset ID.

## Authentication
- Required: `yes`
- Auth type: `bearer token`
- Required roles/scopes: `authenticated user`

## Request
### Headers
- `Content-Type: application/json`
- `Authorization: Bearer <accessToken>`

### Body
```json
{
  "uploadId": "upl_9fd30c",
  "purpose": "profile_photo",
  "checksum": "0f4c2f4f0d1a3f8c7b5e6d4c3b2a19080706050403020100ffeeddccbbaa9988"
}
```

### Validation Rules
- `uploadId`: required, must exist, must belong to current user, must be `pending`.
- `purpose`: required, enum `profile_photo|hub_photo|post_image|document`, and must match the purpose used in presign.
- `checksum`: required, must be the same 64-character SHA-256 hex digest supplied to `POST /uploads/presign`.

## Responses
### Success: `201 Created`
When returned:
- Object exists, passes policy checks, and has been successfully moved to permanent storage.

Body:
```json
{
  "success": true,
  "message": "Upload finalized",
  "data": {
    "assetId": "ast_7b9e10",
    "purpose": "profile_photo",
    "contentType": "image/jpeg",
    "sizeBytes": 734221,
    "url": "https://cdn.example.com/users/usr_123/profile/upl_9fd30c.jpg"
  }
}
```

### Error: `401 Unauthorized`
When returned:
- Missing, invalid, or expired bearer token.

Body:
```json
{"success": false, "error": {"code": "UNAUTHORIZED", "message": "Authentication required.", "details": {}}}
```

### Error: `422 Unprocessable Entity`
When returned:
- Checksum mismatch. The checksum provided at complete time does not match the checksum stored for the presigned upload intent.

Body:
```json
{
  "success": false,
  "error": {
    "code": "UPLOAD_CHECKSUM_MISMATCH",
    "message": "File integrity check failed. Please re-upload.",
    "details": {
      "expectedChecksum": "...",
      "actualChecksum": "..."
    }
  }
}
```

### Error: `409 Conflict`
When returned:
- Object missing, expired, already finalized, or purpose mismatch.

Body:
```json
{
  "success": false,
  "error": {
    "code": "UPLOAD_NOT_FINALIZABLE",
    "message": "Upload cannot be finalized.",
    "details": {}
  }
}
```

## Data & Caching Dependencies
- **Spanner Tables:** `assets (Update/Completed)`
- **Redis Cache:** `None`
- **GCS Storage:** `Upload Bucket (Verify & Move)`
- **Edge Cache (CDN):** `No`

## Side Effects
- Moves upload intent from `pending` to `completed`.
- **File Move**: Moves the physical file from temporary upload path to permanent storage path.
- Creates immutable asset record owned by user.
- **Failures**: If validation or checksum fails, the upload remains unfinalized. Any unfinalized temporary files are cleaned up by a 24-hour GCS Lifecycle Policy.

## Idempotency and Retries
- Idempotent: `conditionally` (repeat with same completed `uploadId` returns same `assetId`).
- Retry guidance: safe to retry on timeout. If `422 UPLOAD_CHECKSUM_MISMATCH` is returned, retry only if the client can supply the exact checksum used during presign; otherwise restart the upload flow.
