# Migrate UAV upload capturedAt to DateTimeOffset

**Task**: AZ-1126_captured_at_datetimeoffset
**Name**: Migrate UavTileMetadata.capturedAt to DateTimeOffset (F-AZ810-2)
**Description**: Close security carry-over F-AZ810-2 by typing `UavTileMetadata.CapturedAt` as `DateTimeOffset` instead of `DateTime`, eliminating ambiguous `DateTimeKind.Unspecified` handling on the UAV upload metadata input path.
**Complexity**: 2 points
**Dependencies**: AZ-810 (HARD — metadata validation layer); AZ-488 (original upload endpoint)
**Component**: SatelliteProvider.Common (UavTileMetadata) + SatelliteProvider.Api (validators) + SatelliteProvider.Services.TileDownloader (quality gate + upload handler)
**Tracker**: AZ-1126
**Epic**: AZ-795

## Problem

Security finding F-AZ810-2 (cycle 8, open through cycle 12) flags that `UavTileMetadata.CapturedAt` is typed `DateTime` rather than `DateTimeOffset`. `DateTime` accepts `DateTimeKind.Unspecified` values that deserialize from offset-less ISO-8601 strings, forcing manual `Kind` normalization in the upload handler and quality gate. This is a time-handling correctness gap that can skew freshness-window checks in non-UTC dev environments.

## Outcome

- `capturedAt` on the UAV upload metadata input is unambiguously UTC-aware at the type level
- Offset-less or `Unspecified` timestamps are rejected before persistence
- F-AZ810-2 is marked resolved in the next security audit cycle
- Wire compatibility preserved for clients sending ISO-8601 UTC with explicit offset (`Z` or `+00:00`)

## Scope

### Included

- Change `UavTileMetadata.CapturedAt` from `DateTime` to `DateTimeOffset`
- Update FluentValidation rules, quality gate, and upload handler to compare via UTC without manual `Kind` branching
- Reject offset-less / ambiguous `capturedAt` values with HTTP 400
- Unit tests for the new rejection path and existing freshness-window rules
- Integration test proving offset-less `capturedAt` is rejected
- Patch `_docs/02_document/contracts/api/uav-tile-upload.md` 1.2.0 → 1.2.1 (change log + clarify offset requirement)

### Excluded

- `TileInventoryEntry.CapturedAt` response field (remains `DateTime?` — DB read path)
- `TileEntity.CapturedAt` persistence layer type
- Changes to gRPC tile delivery or other non-UAV-upload surfaces
- MAJOR contract version bump (wire JSON shape unchanged for compliant clients)

## Acceptance Criteria

**AC-1: Type migration**
Given the UAV upload metadata DTO
When deserialized from JSON
Then `CapturedAt` is `DateTimeOffset` and freshness comparisons use UTC without manual `DateTimeKind` normalization

**AC-2: Reject ambiguous timestamps**
Given a UAV upload batch with `capturedAt` lacking an explicit UTC offset (offset-less ISO string)
When POST `/api/satellite/upload`
Then HTTP 400 with a validation or deserialization error referencing `capturedAt`

**AC-3: Backward-compatible UTC clients**
Given a UAV upload batch with `capturedAt` as ISO-8601 UTC (`...Z` or `...+00:00`)
When POST `/api/satellite/upload` with otherwise valid payload
Then HTTP 200 (or the same non-timestamp rejection as before timestamp validation)

**AC-4: Contract patch**
Given the implementation is complete
When `uav-tile-upload.md` is reviewed
Then version is 1.2.1 with a change-log entry documenting the offset requirement and F-AZ810-2 closure

## Non-Functional Requirements

**Compatibility**
- Compliant clients already sending `Z`-suffixed timestamps must not break

**Security**
- Closes F-AZ810-2 (Low / informational time-handling finding)

## Unit Tests

| AC Ref | What to Test | Required Outcome |
|--------|-------------|-----------------|
| AC-1 | `UavTileMetadataValidator` freshness window with `DateTimeOffset` | Future/too-old rules still fire |
| AC-2 | Deserializer or validator with offset-less `capturedAt` | Rejected |
| AC-1 | `UavTileQualityGate` captured-at rules | Still accept/reject correctly |

## Blackbox Tests

| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|--------|------------------------|-------------|-------------------|----------------|
| AC-2 | Valid JPEG + metadata with `capturedAt: "2026-06-26T12:00:00"` (no offset) | POST `/api/satellite/upload` | HTTP 400 mentioning `capturedAt` | — |
| AC-3 | Valid JPEG + metadata with `capturedAt` as `DateTime.UtcNow.ToString("o")` | POST `/api/satellite/upload` | HTTP 200 (happy path unchanged) | Compatibility |

## Constraints

- Must remain a child of epic AZ-795 strict-validation theme
- No breaking wire change for offset-aware clients

## Risks & Mitigation

**Risk 1: Client sends offset-less timestamps**
- *Risk*: Legitimate clients using `"2026-06-26T12:00:00"` without `Z` start failing
- *Mitigation*: Contract patch documents requirement; rejection is intentional per F-AZ810-2

## Contract

This task patches the producer contract at `_docs/02_document/contracts/api/uav-tile-upload.md` (1.2.0 → 1.2.1).

Consumers: `gps-denied-onboard`, mission planner UI, any UAV upload client.

### Document Dependencies

- `_docs/02_document/contracts/api/uav-tile-upload.md` v1.2.0 (patch to 1.2.1)
- `_docs/02_document/contracts/api/error-shape.md` v1.0.0 (unchanged)
