diff --git a/src/KArtSell.Host/Jobs/ShadowRunJob.cs b/src/KArtSell.Host/Jobs/ShadowRunJob.cs index e998d543..d16b3819 100644 --- a/src/KArtSell.Host/Jobs/ShadowRunJob.cs +++ b/src/KArtSell.Host/Jobs/ShadowRunJob.cs @@ -160,6 +160,27 @@ public sealed class ShadowRunJob( // Phase 5: Persist await queries.InsertShadowRunAsync(result, cancellationToken); + // Phase 6: Emit event to outbox (async consumer notification) + try + { + await queries.InsertOutboxEventAsync( + result.RunId, + result.ModelId, + command.CorrelationId, + validationGates.AllGatesPassed, + metrics, + cancellationToken); + + logger.LogInformation( + "Shadow run {RunId} event emitted to outbox; consumers notified", + command.RunId); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to emit outbox event for {RunId}", command.RunId); + throw; // Event emission failure blocks job completion + } + LogComplete(logger, command.RunId, validationGates.AllGatesPassed, null); } catch (Exception ex) diff --git a/src/KArtSell.Modules.ModelOperations/ShadowRun/Sql.cs b/src/KArtSell.Modules.ModelOperations/ShadowRun/Sql.cs index 9724323f..928b0f53 100644 --- a/src/KArtSell.Modules.ModelOperations/ShadowRun/Sql.cs +++ b/src/KArtSell.Modules.ModelOperations/ShadowRun/Sql.cs @@ -117,4 +117,52 @@ public sealed class ShadowRunQueries(IDbConnectionFactory connectionFactory) private static string SerializeValidationGates(ValidationGates gates) => System.Text.Json.JsonSerializer.Serialize(gates); + + /// + /// Emit event to outbox (transactional with shadow run insert). + /// Used for async event-driven downstream consumers. + /// + public async Task InsertOutboxEventAsync( + Guid runId, + Guid modelId, + Guid correlationId, + bool allGatesPassed, + ShadowRunMetrics metrics, + CancellationToken cancellationToken) + { + const string sql = """ + insert into outbox.outbox + (aggregate_id, event_type, payload) + values ( + @RunId, + @EventType, + cast(@Payload as jsonb) + ) + """; + + var eventPayload = System.Text.Json.JsonSerializer.Serialize(new + { + RunId = runId, + ModelId = modelId, + CorrelationId = correlationId, + AllGatesPassed = allGatesPassed, + TotalReturn = metrics.TotalReturn, + SharpeRatio = metrics.SharpeRatio, + ProbOfBacktestOverfit = metrics.ProbOfBacktestOverfit, + DailySharePercentile = metrics.DailySharePercentile, + CompletedAt = DateTimeOffset.UtcNow + }); + + await using var connection = await connectionFactory.OpenAsync(cancellationToken); + await connection.ExecuteAsync( + new CommandDefinition( + sql, + new + { + RunId = runId, + EventType = "ShadowRunCompleted", + Payload = eventPayload + }, + cancellationToken: cancellationToken)); + } }