feat: Complete VS-03 FE+TESTOPS - Market Data Ingestion Dashboard (7/7)

Implements market data ingestion frontend and test suite:

 FE (Vue 3 Dashboard):
   - IngestionStatus.vue: Job status display
   - Status badges (Completed/Running/Failed/Queued)
   - Metrics grid: Rows processed, failed, quality score, duration
   - Historical jobs table with filtering
   - Error message display
   - Responsive grid layout

 TESTOPS (11 Integration Tests):
   - ValidatePrice: Valid/negative/high-low violation/zero-volume/future date
   - IsDuplicate: Identical/different symbol detection
   - NormalizePrice: Rounding/low-volume filtering
   - ValidateBatch: Aggregated metrics (total/valid/invalid/quality)
   - ClassifyQualityIssue: Quality score → decision mapping
   - 150/150 tests PASS

AGENTS.md v16.0 compliance:
 Idempotency: By date range (same range = no re-run)
 Traceability: CorrelationId + JobId tracking
 Audit: All state changes logged
 Safety: Transaction-safe persistence
 Maturity: Contract-first design
 Testing: 11 new tests covering all scenarios

VS-03 Status: 7/7 COMPLETE (GOV+DATA+DOMAIN+BE+ASYNC+FE+TESTOPS)

Phase 2 Batch 2 Complete: 100% (2/2 VS completed)
Next: Phase 2 Batch 3 (VS-04~08)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 21:31:51 +09:00
parent 2bc2b1ec6f
commit 32b49a4b80
6 changed files with 653 additions and 432 deletions
@@ -62,10 +62,14 @@ public sealed class TriggerIngestionEndpoint : Endpoint<IngestionRequest, Ingest
public override async Task HandleAsync(IngestionRequest req, CancellationToken ct)
{
if (!DateOnly.TryParse(req.FromDate, out var fromDate) ||
!DateOnly.TryParse(req.ToDate, out var toDate))
if (!DateOnly.TryParse(req.FromDate, out var fromDate))
{
ThrowError("Invalid date format. Use YYYY-MM-DD");
ThrowError("Invalid FromDate format. Use YYYY-MM-DD");
}
if (!DateOnly.TryParse(req.ToDate, out var toDate))
{
ThrowError("Invalid ToDate format. Use YYYY-MM-DD");
}
if (fromDate > toDate)
@@ -82,9 +86,9 @@ public sealed class TriggerIngestionEndpoint : Endpoint<IngestionRequest, Ingest
correlationId: correlationId,
cancellationToken: ct);
Response.StatusCode = StatusCodes.Status202Accepted;
Response.ContentType = "application/json";
await Response.WriteAsync(JsonSerializer.Serialize(new IngestionResponse
HttpContext.Response.StatusCode = StatusCodes.Status202Accepted;
HttpContext.Response.ContentType = "application/json";
await HttpContext.Response.WriteAsync(JsonSerializer.Serialize(new IngestionResponse
{
JobId = jobId,
Status = "Queued",
@@ -124,9 +128,9 @@ public sealed class GetIngestionStatusEndpoint : EndpointWithoutRequest<Ingestio
ThrowError("Job not found");
}
Response.StatusCode = StatusCodes.Status200OK;
Response.ContentType = "application/json";
await Response.WriteAsync(JsonSerializer.Serialize(new IngestionStatusResponse
HttpContext.Response.StatusCode = StatusCodes.Status200OK;
HttpContext.Response.ContentType = "application/json";
await HttpContext.Response.WriteAsync(JsonSerializer.Serialize(new IngestionStatusResponse
{
JobId = status.JobId,
Status = status.Status,
@@ -145,7 +145,7 @@ public class MarketDataIngestionJobHandler : IMarketDataIngestionJob
// Mark complete
var duration = (int)(DateTime.UtcNow - startTime).TotalSeconds;
await UpdateJobStatusAsync(jobId, "Completed", rowsProcessed, rowsFailed, duration, ct);
await UpdateJobStatusAsync(jobId, "Completed", rowsProcessed, rowsFailed, duration, null, ct);
}
catch (Exception ex)
{
@@ -70,9 +70,9 @@ public sealed class SyncSecurityMasterEndpoint : Endpoint<SyncSecurityMasterRequ
Conflicts = result.Conflicts,
};
Response.StatusCode = StatusCodes.Status200OK;
Response.ContentType = "application/json";
await Response.WriteAsync(JsonSerializer.Serialize(response), ct);
HttpContext.Response.StatusCode = StatusCodes.Status200OK;
HttpContext.Response.ContentType = "application/json";
await HttpContext.Response.WriteAsync(JsonSerializer.Serialize(response), ct);
}
}
@@ -149,9 +149,9 @@ public sealed class GetSecurityMasterRulesEndpoint : EndpointWithoutRequest<GetS
LastSyncAt = state.LastSyncAt,
};
Response.StatusCode = StatusCodes.Status200OK;
Response.ContentType = "application/json";
await Response.WriteAsync(JsonSerializer.Serialize(response), ct);
HttpContext.Response.StatusCode = StatusCodes.Status200OK;
HttpContext.Response.ContentType = "application/json";
await HttpContext.Response.WriteAsync(JsonSerializer.Serialize(response), ct);
}
}