# Exam Sprint Report Filename Design

## Goal

Update 临考词汇突击 PDF report filenames so generated files use the student's name, the business report title, and an `Asia/Shanghai` timestamp.

## Scope

This change is limited to the exam-sprint report generation filename path and the minimal payload/domain/storage-test propagation needed to make `studentName` available for both report types and keep generated download URLs working with localized filenames.

It changes the `fileName` passed to `ExamSprintReportStorage.upload(...)`. Because the existing storage implementations return the same value as `storageObjectKey` in current tests and Azure uses `fileName` as the blob name, the visible storage key and downloaded filename will also change.

This change does not alter:

- API response schemas
- report rendering content
- PDF generation
- storage interfaces and Azure storage behavior
- download URL generation semantics
- report state transitions

## Recommended Approach

Keep filename construction centralized in `ExamSprintReportGenerationPipeline.fileNameOf(...)`.

The pipeline already owns the upload filename and has access to `ExamSprintReport`, `ReportType`, report content, and the injected `Clock`. This is the smallest safe change and keeps Azure storage, in-memory storage, logs, and persisted `fileName`/`storageObjectKey` values consistent. For achievement reports, add optional `studentName` propagation from the request contract into `AchievementReportContent` so the pipeline can use the same root field rule as outlook reports.

## Business Rules

### Filename format

Generated PDF filenames must use one of these formats:

```text
{StudentName}-临考词汇突击成果报告-{currentTime}.pdf
{StudentName}-临考词汇突击潜力展望报告-{currentTime}.pdf
```

Examples:

```text
吴泓妤-临考词汇突击成果报告-20250627133544.pdf
冯亿豪-临考词汇突击潜力展望报告-20250627120841.pdf
```

### Student name source

- Read lowercase `studentName` from the report payload root for filename generation.
- If `studentName` is missing, JSON `null`, non-textual, empty, or all whitespace, use `reportId` as the filename prefix.
- Trim leading and trailing whitespace before using `studentName`.
- Outlook validation must continue to accept the existing upstream `StudentName` field and may also accept lowercase `studentName` as an alias, without letting the filename-only lowercase field weaken unrelated validation.

### Report title mapping

- `ReportType.ACHIEVEMENT` maps to `临考词汇突击成果报告`.
- `ReportType.OUTLOOK` maps to `临考词汇突击潜力展望报告`.

### Timestamp

- Use the pipeline's injected `Clock`.
- Format the current instant in `Asia/Shanghai`.
- Use pattern `yyyyMMddHHmmss`.
- Keep the `.pdf` extension.

## Components

### `ExamSprintReportGenerationPipeline`

Update `fileNameOf(ExamSprintReport report)` to build the business filename instead of `exam-sprint-{type}-report-{reportId}.pdf`.

Add focused private helpers if needed:

- `studentNameOrReportId(ExamSprintReport report)`
- `studentNameFromContent(ReportContent content)`
- `reportTitle(ReportType reportType)`
- `formattedShanghaiTimestamp()`

The implementation should support both current content shapes:

- `UnmodeledReportContent` wrapping a `JsonNode` source, used by outlook reports.
- `AchievementReportContent`, used by achievement reports after payload validation and mapping, with optional `studentName` propagated from `AchievementExamSprintReportPayload.studentName`.

If an achievement payload omits `studentName`, the implementation should still fall back to `reportId` rather than failing.

### `AchievementExamSprintReportPayload`

Add optional root field `studentName` without `@NotBlank`, because missing or blank names are valid and must fall back to `reportId`.

### `OutlookExamSprintReportPayload`

Keep the existing `@JsonProperty("StudentName")` contract and add lowercase `studentName` as a Jackson alias for validation compatibility. When both `StudentName` and `studentName` appear, validation should avoid treating the filename-only alias as a replacement for the canonical upstream name, while the persisted raw payload still keeps lowercase `studentName` for filename generation.

### `AchievementReportContent` and `AchievementReportContentMapper`

Add optional `studentName` to `AchievementReportContent` and map it from the contract payload. Existing validation should continue to require the current report content fields only.

### `InMemoryExamSprintReportStorage`

Stop deriving `reportId` from `storageObjectKey` filename prefixes. The new localized filenames are not parseable by the old `exam-sprint-*-report-{reportId}.pdf` convention. The in-memory storage should retain the `reportId` supplied to `upload(...)` with the stored file and use that value in `generateDownloadUrl(...)`.

## Data Flow

1. The application service validates and saves the submitted report.
2. The generation pipeline renders HTML and generates PDF bytes.
3. For achievement reports, `AchievementReportContentMapper` preserves root `studentName` on the domain content. For outlook reports, the unmodeled JSON content preserves root lowercase `studentName` while validation remains compatible with canonical `StudentName`.
4. Before uploading, the pipeline builds the filename from report content, report type, report id, and `Clock`.
5. `storage.upload(...)` receives the new filename.
6. The stored file result is persisted on `ExamSprintReport` as `storageObjectKey` and `fileName`.
7. Existing query, download, cleanup, and logging flows continue to use the persisted values.

## Error Handling

- Missing or invalid `studentName` must not fail generation.
- Unknown report type should fail fast through the existing pipeline exception handling rather than producing a misleading filename.
- Time formatting should be deterministic under tests by using the injected `Clock`.

## Testing Strategy

Follow TDD:

1. Update application/generation tests to expect the new filename format and verify they fail against current code.
2. Implement the minimal filename-generation changes.
3. Run the focused application tests.
4. Run the full Maven test suite.

Required coverage:

- Outlook report with lowercase `studentName` stores `冯亿豪-临考词汇突击潜力展望报告-20260101080000.pdf` when `Clock` is fixed at `2026-01-01T00:00:00Z`.
- Achievement report with lowercase `studentName` stores `吴泓妤-临考词汇突击成果报告-20260101080000.pdf` for the same clock instant.
- Missing or blank `studentName` falls back to `{reportId}-临考词汇突击潜力展望报告-20260101080000.pdf` or the corresponding achievement title.
- Existing storage key, persisted `fileName`, generated download key tracking, and log assertions are updated to the same new filename.
- Default in-memory storage can generate download URLs after localized filename upload without parsing report id from the filename.

## Acceptance Criteria

- Generated outlook PDF filenames follow `{StudentNameOrReportId}-临考词汇突击潜力展望报告-{yyyyMMddHHmmss}.pdf`.
- Generated achievement PDF filenames follow `{StudentNameOrReportId}-临考词汇突击成果报告-{yyyyMMddHHmmss}.pdf`.
- `currentTime` is formatted in `Asia/Shanghai`.
- Empty, missing, null, or non-textual `studentName` uses `reportId`.
- In-memory storage download URL generation remains compatible with localized filenames.
- All affected tests pass.
