fix: DEBT-018 - make outbox writes co-transactional with entity writes
TradeExecution: TradeOutboxPublisher.PublishAsync replaced with UpdateAndPublishAsync, which opens one connection/transaction, updates trade status and writes the outbox message on it, then commits once. Used by the 3 call sites that publish an event after a status update (SubmitTradeHandler, PollTradeStatusHandler's FullyFilled branch, ConfirmSettlementHandler). PortfolioReconciliation: ReconcileTradeHandler now injects the request-scoped IDbConnection (the same instance ReconciliationSql already uses) instead of opening a second separate connection via IDbConnectionFactory, begins one transaction shared by ReconciliationEngine.ReconcileTradeAsync and the outbox writes, and commits once. Required adding IDbTransaction-aware overloads of GetHoldingAsync/UpsertHoldingAsync/InsertReconciliationLogAsync - the read needed one too, since Npgsql throws if a command on a connection with a pending transaction doesn't have it attached. dotnet build KArtSell.sln -c Release: clean. Integration tests: 17 pure-logic tests pass, 13 DB-backed tests fail with the pre-existing connection-refused error (no SSH tunnel in this environment) - the transactional changes themselves are not yet verified against a live database. Also corrected two WBS_PROGRESS_TRACKER.csv rows that inaccurately said frontend UI work was "in progress" when the background agents building VS-28/VS-29 UI had actually failed (hit the session's spend limit) before committing anything. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+24
-15
@@ -1,9 +1,10 @@
|
||||
namespace KArtSell.Modules.ModelOperations.PortfolioReconciliation;
|
||||
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using KArtSell.BuildingBlocks.Data;
|
||||
using KArtSell.BuildingBlocks.Hashing;
|
||||
using KArtSell.BuildingBlocks.Reliability;
|
||||
using KArtSell.BuildingBlocks.Time;
|
||||
@@ -31,18 +32,18 @@ public class ReconcileTradeCommand
|
||||
public class ReconcileTradeHandler
|
||||
{
|
||||
private readonly ReconciliationEngine _engine;
|
||||
private readonly IDbConnectionFactory _connectionFactory;
|
||||
private readonly IDbConnection _connection;
|
||||
private readonly IOutboxWriter _outboxWriter;
|
||||
private readonly IClock _clock;
|
||||
|
||||
public ReconcileTradeHandler(
|
||||
ReconciliationEngine engine,
|
||||
IDbConnectionFactory connectionFactory,
|
||||
IDbConnection connection,
|
||||
IOutboxWriter outboxWriter,
|
||||
IClock clock)
|
||||
{
|
||||
_engine = engine ?? throw new ArgumentNullException(nameof(engine));
|
||||
_connectionFactory = connectionFactory ?? throw new ArgumentNullException(nameof(connectionFactory));
|
||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
_outboxWriter = outboxWriter ?? throw new ArgumentNullException(nameof(outboxWriter));
|
||||
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
|
||||
}
|
||||
@@ -51,6 +52,13 @@ public class ReconcileTradeHandler
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(command);
|
||||
|
||||
// DEBT-018: share one connection/transaction across the engine's holding/log writes and
|
||||
// the outbox event(s) below, instead of the engine writing on its own connection and the
|
||||
// outbox writing on a second, separately-committed one. _connection is scoped per HTTP
|
||||
// request (see Program.cs), the same instance ReconciliationEngine/ReconciliationSql use,
|
||||
// so this is the same underlying connection, not a new one.
|
||||
using var transaction = _connection.BeginTransaction();
|
||||
|
||||
var result = await _engine.ReconcileTradeAsync(
|
||||
command.TradeId,
|
||||
command.SecurityId,
|
||||
@@ -61,10 +69,12 @@ public class ReconcileTradeHandler
|
||||
command.TradeDate,
|
||||
command.ExpectedSettlementDate,
|
||||
command.ActualSettlementDate,
|
||||
command.CorrelationId);
|
||||
command.CorrelationId,
|
||||
transaction);
|
||||
|
||||
if (!result.Success)
|
||||
{
|
||||
transaction.Rollback();
|
||||
throw new InvalidOperationException($"Reconciliation failed: {result.Error}");
|
||||
}
|
||||
|
||||
@@ -86,7 +96,7 @@ public class ReconcileTradeHandler
|
||||
IdempotencyKey = command.IdempotencyKey ?? Guid.NewGuid().ToString()
|
||||
};
|
||||
|
||||
await PublishAsync("TradeReconciled", @event, command.CorrelationId, CancellationToken.None);
|
||||
await PublishAsync(transaction, "TradeReconciled", @event, command.CorrelationId, CancellationToken.None);
|
||||
|
||||
// If mismatches detected, publish alert event
|
||||
if (result.MismatchDetected)
|
||||
@@ -103,16 +113,18 @@ public class ReconcileTradeHandler
|
||||
CorrelationId = command.CorrelationId
|
||||
};
|
||||
|
||||
await PublishAsync("ReconciliationMismatchAlert", alertEvent, command.CorrelationId, CancellationToken.None);
|
||||
await PublishAsync(transaction, "ReconciliationMismatchAlert", alertEvent, command.CorrelationId, CancellationToken.None);
|
||||
}
|
||||
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DEBT-TRADE-001: outbox write happens in its own transaction, separate from the
|
||||
/// preceding holding/log writes owned by ReconciliationSql. Not yet atomic with the
|
||||
/// entity write. See TECH_DEBT_REGISTER.md.
|
||||
/// DEBT-018 (fixed): writes to the same transaction the holding/log writes used above, so a
|
||||
/// crash between the entity write and the outbox write can no longer leave one committed
|
||||
/// without the other.
|
||||
/// </summary>
|
||||
private async Task PublishAsync<T>(string eventType, T @event, Guid correlationId, CancellationToken ct) where T : class
|
||||
private async Task PublishAsync<T>(IDbTransaction transaction, string eventType, T @event, Guid correlationId, CancellationToken ct) where T : class
|
||||
{
|
||||
var payload = JsonSerializer.Serialize(@event);
|
||||
var message = new OutboxMessage(
|
||||
@@ -124,10 +136,7 @@ public class ReconcileTradeHandler
|
||||
_clock.UtcNow,
|
||||
ContentHasher.Sha256(payload));
|
||||
|
||||
await using var connection = await _connectionFactory.OpenAsync(ct);
|
||||
await using var transaction = await connection.BeginTransactionAsync(ct);
|
||||
await _outboxWriter.AddAsync(connection, transaction, message, ct);
|
||||
await transaction.CommitAsync(ct);
|
||||
await _outboxWriter.AddAsync((DbConnection)_connection, (DbTransaction)transaction, message, ct);
|
||||
}
|
||||
|
||||
private int CountBysSeverity(List<Mismatch> mismatches, MismatchSeverity severity)
|
||||
|
||||
+13
-6
@@ -41,15 +41,18 @@ public class ReconciliationEngine
|
||||
DateTime tradeDate,
|
||||
DateTime expectedSettlementDate,
|
||||
DateTime? actualSettlementDate,
|
||||
Guid correlationId)
|
||||
Guid correlationId,
|
||||
System.Data.IDbTransaction? transaction = null)
|
||||
{
|
||||
var result = new ReconciliationResult { CorrelationId = correlationId };
|
||||
var now = _clock.UtcNow.UtcDateTime;
|
||||
|
||||
try
|
||||
{
|
||||
// Load current holding
|
||||
var holding = await _repository.GetHoldingAsync(securityId);
|
||||
// Load current holding. DEBT-018: must use the transaction-aware overload once a
|
||||
// transaction is active on the shared connection (see ReconcileTradeHandler), or
|
||||
// Npgsql throws on this read.
|
||||
var holding = await _repository.GetHoldingAsync(securityId, transaction);
|
||||
if (holding == null)
|
||||
{
|
||||
holding = new Holding
|
||||
@@ -110,8 +113,9 @@ public class ReconciliationEngine
|
||||
|
||||
var mismatchDetected = mismatches.Count > 0;
|
||||
|
||||
// Save holding
|
||||
await _repository.UpsertHoldingAsync(holding);
|
||||
// Save holding. DEBT-018: when a transaction is supplied, the write commits
|
||||
// atomically with the outbox event ReconcileTradeHandler publishes afterward.
|
||||
await _repository.UpsertHoldingAsync(holding, transaction);
|
||||
|
||||
// Log reconciliation
|
||||
var logEntry = new ReconciliationLog
|
||||
@@ -130,7 +134,7 @@ public class ReconciliationEngine
|
||||
CorrelationId = correlationId
|
||||
};
|
||||
|
||||
await _repository.InsertReconciliationLogAsync(logEntry);
|
||||
await _repository.InsertReconciliationLogAsync(logEntry, transaction);
|
||||
|
||||
result.Success = true;
|
||||
result.Holding = holding;
|
||||
@@ -255,8 +259,11 @@ public class ReconciliationLog
|
||||
public interface IReconciliationRepository
|
||||
{
|
||||
Task<Holding?> GetHoldingAsync(Guid securityId);
|
||||
Task<Holding?> GetHoldingAsync(Guid securityId, System.Data.IDbTransaction? transaction);
|
||||
Task UpsertHoldingAsync(Holding holding);
|
||||
Task UpsertHoldingAsync(Holding holding, System.Data.IDbTransaction? transaction);
|
||||
Task InsertReconciliationLogAsync(ReconciliationLog log);
|
||||
Task InsertReconciliationLogAsync(ReconciliationLog log, System.Data.IDbTransaction? transaction);
|
||||
Task<List<ReconciliationLog>> GetReconciliationLogsAsync(DateTime startDate, DateTime endDate);
|
||||
Task<List<Holding>> GetOpenHoldingsAsync();
|
||||
}
|
||||
|
||||
@@ -20,7 +20,15 @@ public class ReconciliationSql : IReconciliationRepository
|
||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
}
|
||||
|
||||
public async Task<Holding?> GetHoldingAsync(Guid securityId)
|
||||
public Task<Holding?> GetHoldingAsync(Guid securityId) => GetHoldingAsync(securityId, transaction: null);
|
||||
|
||||
/// <summary>
|
||||
/// DEBT-018: once a transaction is active on the shared connection (see
|
||||
/// ReconcileTradeHandler), every command on that connection must be given the transaction
|
||||
/// explicitly or Npgsql throws — this overload is required, not optional, when a caller has
|
||||
/// called BeginTransaction() on the same IDbConnection instance.
|
||||
/// </summary>
|
||||
public async Task<Holding?> GetHoldingAsync(Guid securityId, IDbTransaction? transaction)
|
||||
{
|
||||
const string sql = """
|
||||
SELECT id, security_id, quantity, weighted_avg_cost, total_cost_basis,
|
||||
@@ -34,10 +42,17 @@ public class ReconciliationSql : IReconciliationRepository
|
||||
|
||||
return await _connection.QueryFirstOrDefaultAsync<Holding>(
|
||||
sql,
|
||||
new { security_id = securityId });
|
||||
new { security_id = securityId },
|
||||
transaction);
|
||||
}
|
||||
|
||||
public async Task UpsertHoldingAsync(Holding holding)
|
||||
public Task UpsertHoldingAsync(Holding holding) => UpsertHoldingAsync(holding, transaction: null);
|
||||
|
||||
/// <summary>
|
||||
/// DEBT-018: pass the transaction the caller is also using for the outbox write (see
|
||||
/// ReconcileTradeHandler) so the holding update and the outbox event commit atomically.
|
||||
/// </summary>
|
||||
public async Task UpsertHoldingAsync(Holding holding, IDbTransaction? transaction)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO portfolio_management.holdings
|
||||
@@ -57,10 +72,16 @@ public class ReconciliationSql : IReconciliationRepository
|
||||
revision = revision + 1
|
||||
""";
|
||||
|
||||
await _connection.ExecuteAsync(sql, holding);
|
||||
await _connection.ExecuteAsync(sql, holding, transaction);
|
||||
}
|
||||
|
||||
public async Task InsertReconciliationLogAsync(ReconciliationLog log)
|
||||
public Task InsertReconciliationLogAsync(ReconciliationLog log) => InsertReconciliationLogAsync(log, transaction: null);
|
||||
|
||||
/// <summary>
|
||||
/// DEBT-018: pass the transaction the caller is also using for the outbox write (see
|
||||
/// ReconcileTradeHandler) so the log entry and the outbox event commit atomically.
|
||||
/// </summary>
|
||||
public async Task InsertReconciliationLogAsync(ReconciliationLog log, IDbTransaction? transaction)
|
||||
{
|
||||
const string sql = """
|
||||
INSERT INTO portfolio_management.reconciliation_logs
|
||||
@@ -86,7 +107,7 @@ public class ReconciliationSql : IReconciliationRepository
|
||||
log.ReconciledAt,
|
||||
log.PublishedAt,
|
||||
log.CorrelationId
|
||||
});
|
||||
}, transaction);
|
||||
}
|
||||
|
||||
public async Task<List<ReconciliationLog>> GetReconciliationLogsAsync(
|
||||
|
||||
@@ -57,9 +57,13 @@ public class SubmitTradeHandler
|
||||
);
|
||||
|
||||
trade.MarkSubmitted(orderId, response);
|
||||
await _sql.UpdateTradeStatusAsync(trade, response, null, ct);
|
||||
|
||||
await PublishEventAsync(
|
||||
await TradeOutboxPublisher.UpdateAndPublishAsync(
|
||||
_connectionFactory,
|
||||
_sql,
|
||||
_outbox,
|
||||
_clock,
|
||||
trade,
|
||||
response,
|
||||
"TradeSubmitted",
|
||||
new TradeSubmittedEvent
|
||||
{
|
||||
@@ -87,9 +91,6 @@ public class SubmitTradeHandler
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PublishEventAsync<T>(string eventType, T @event, Guid correlationId, CancellationToken ct) where T : class
|
||||
=> await TradeOutboxPublisher.PublishAsync(_connectionFactory, _outbox, _clock, eventType, @event, correlationId, ct);
|
||||
}
|
||||
|
||||
public class PollTradeStatusCommand
|
||||
@@ -149,14 +150,15 @@ public class PollTradeStatusHandler
|
||||
trade.MarkFilled(executedQty, unitPrice, response, _clock.UtcNow.UtcDateTime);
|
||||
}
|
||||
|
||||
await _sql.UpdateTradeStatusAsync(trade, response, null, ct);
|
||||
|
||||
if (trade.Status is TradeStatus.FullyFilled)
|
||||
{
|
||||
await TradeOutboxPublisher.PublishAsync(
|
||||
await TradeOutboxPublisher.UpdateAndPublishAsync(
|
||||
_connectionFactory,
|
||||
_sql,
|
||||
_outbox,
|
||||
_clock,
|
||||
trade,
|
||||
response,
|
||||
"TradeFilled",
|
||||
new TradeFilledEvent
|
||||
{
|
||||
@@ -168,6 +170,10 @@ public class PollTradeStatusHandler
|
||||
command.CorrelationId,
|
||||
ct);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _sql.UpdateTradeStatusAsync(trade, response, null, ct);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Trade status updated: {TradeId} -> {Status}", trade.Id, status);
|
||||
}
|
||||
@@ -234,12 +240,13 @@ public class ConfirmSettlementHandler
|
||||
if (success)
|
||||
{
|
||||
trade.MarkConfirmed(_clock.UtcNow.UtcDateTime, command.Commission);
|
||||
await _sql.UpdateTradeStatusAsync(trade, response, null, ct);
|
||||
|
||||
await TradeOutboxPublisher.PublishAsync(
|
||||
await TradeOutboxPublisher.UpdateAndPublishAsync(
|
||||
_connectionFactory,
|
||||
_sql,
|
||||
_outbox,
|
||||
_clock,
|
||||
trade,
|
||||
response,
|
||||
"TradeSettled",
|
||||
new TradeSettledEvent
|
||||
{
|
||||
@@ -287,16 +294,19 @@ public class TradeSettledEvent
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DEBT-TRADE-001: outbox write happens in its own transaction, separate from the
|
||||
/// preceding trade status update (which owns its own connection in TradeSql). Not yet
|
||||
/// atomic with the state transition. See TECH_DEBT_REGISTER.md.
|
||||
/// DEBT-018 (fixed): the trade status update and the outbox event write now share one
|
||||
/// connection/transaction, committed together, instead of the status update committing on
|
||||
/// TradeSql's own connection and the outbox write committing separately afterward.
|
||||
/// </summary>
|
||||
internal static class TradeOutboxPublisher
|
||||
{
|
||||
public static async Task PublishAsync<T>(
|
||||
public static async Task UpdateAndPublishAsync<T>(
|
||||
IDbConnectionFactory connectionFactory,
|
||||
ITradeSql sql,
|
||||
IOutboxWriter outbox,
|
||||
IClock clock,
|
||||
Trade trade,
|
||||
System.Text.Json.JsonElement? kisResponse,
|
||||
string eventType,
|
||||
T @event,
|
||||
Guid correlationId,
|
||||
@@ -314,7 +324,10 @@ internal static class TradeOutboxPublisher
|
||||
|
||||
await using var connection = await connectionFactory.OpenAsync(ct);
|
||||
await using var transaction = await connection.BeginTransactionAsync(ct);
|
||||
|
||||
await sql.UpdateTradeStatusAsync(connection, transaction, trade, kisResponse, errorMessage: null, ct);
|
||||
await outbox.AddAsync(connection, transaction, message, ct);
|
||||
|
||||
await transaction.CommitAsync(ct);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Data.Common;
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -13,6 +14,13 @@ public interface ITradeSql
|
||||
Task<IEnumerable<Trade>> GetTradesByDecisionIdAsync(Guid sellDecisionId, Guid correlationId, CancellationToken ct = default);
|
||||
Task InsertTradeAsync(Trade trade, CancellationToken ct = default);
|
||||
Task UpdateTradeStatusAsync(Trade trade, JsonElement? kisResponse, string? errorMessage, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// DEBT-018: transaction-aware overload so the caller can write the outbox event on the same
|
||||
/// connection/transaction as this status update, committing both atomically. See
|
||||
/// TradeOutboxPublisher/SubmitTradeHandler.
|
||||
/// </summary>
|
||||
Task UpdateTradeStatusAsync(DbConnection connection, DbTransaction transaction, Trade trade, JsonElement? kisResponse, string? errorMessage, CancellationToken ct = default);
|
||||
Task<int> CountTradesByStatusAsync(TradeStatus status, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -154,6 +162,47 @@ public class TradeSql : ITradeSql
|
||||
_logger.LogInformation("Inserted trade {TradeId}", trade.Id);
|
||||
}
|
||||
|
||||
private const string UpdateTradeStatusSql = """
|
||||
INSERT INTO model_operations.trade_status_history
|
||||
(id, trade_id, old_status, new_status, transitioned_at, kis_response, error_message, published_at, correlation_id)
|
||||
SELECT @id, id, status, @newStatus, NOW(), @kisResponse::jsonb, @errorMessage, NOW(), @correlationId
|
||||
FROM model_operations.trades
|
||||
WHERE id = @tradeId;
|
||||
|
||||
UPDATE model_operations.trades
|
||||
SET status = @newStatus,
|
||||
kis_order_id = COALESCE(@kisOrderId, kis_order_id),
|
||||
executed_quantity = COALESCE(@executedQuantity, executed_quantity),
|
||||
unit_price = COALESCE(@unitPrice, unit_price),
|
||||
total_amount = COALESCE(@totalAmount, total_amount),
|
||||
commission = COALESCE(@commission, commission),
|
||||
net_proceeds = COALESCE(@netProceeds, net_proceeds),
|
||||
execution_timestamp = COALESCE(@executionTimestamp, execution_timestamp),
|
||||
settlement_timestamp = COALESCE(@settlementTimestamp, settlement_timestamp),
|
||||
kis_response = COALESCE(@kisResponse::jsonb, kis_response),
|
||||
error_message = COALESCE(@errorMessage, error_message),
|
||||
revision = revision + 1
|
||||
WHERE id = @tradeId
|
||||
""";
|
||||
|
||||
private static object BuildUpdateTradeStatusParams(Trade trade, JsonElement? kisResponse, string? errorMessage) => new
|
||||
{
|
||||
id = Guid.NewGuid(),
|
||||
tradeId = trade.Id,
|
||||
newStatus = trade.Status.ToString(),
|
||||
kisOrderId = trade.KisOrderId,
|
||||
executedQuantity = trade.ExecutedQuantity,
|
||||
unitPrice = trade.UnitPrice,
|
||||
totalAmount = trade.TotalAmount,
|
||||
commission = trade.Commission,
|
||||
netProceeds = trade.NetProceeds,
|
||||
executionTimestamp = trade.ExecutionTimestamp,
|
||||
settlementTimestamp = trade.SettlementTimestamp,
|
||||
kisResponse = kisResponse?.ToString(),
|
||||
errorMessage,
|
||||
correlationId = trade.CorrelationId
|
||||
};
|
||||
|
||||
public async Task UpdateTradeStatusAsync(
|
||||
Trade trade,
|
||||
JsonElement? kisResponse,
|
||||
@@ -161,51 +210,28 @@ public class TradeSql : ITradeSql
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
using var connection = await _dataSource.OpenConnectionAsync(ct);
|
||||
|
||||
const string sql = """
|
||||
INSERT INTO model_operations.trade_status_history
|
||||
(id, trade_id, old_status, new_status, transitioned_at, kis_response, error_message, published_at, correlation_id)
|
||||
SELECT @id, id, status, @newStatus, NOW(), @kisResponse::jsonb, @errorMessage, NOW(), @correlationId
|
||||
FROM model_operations.trades
|
||||
WHERE id = @tradeId;
|
||||
|
||||
UPDATE model_operations.trades
|
||||
SET status = @newStatus,
|
||||
kis_order_id = COALESCE(@kisOrderId, kis_order_id),
|
||||
executed_quantity = COALESCE(@executedQuantity, executed_quantity),
|
||||
unit_price = COALESCE(@unitPrice, unit_price),
|
||||
total_amount = COALESCE(@totalAmount, total_amount),
|
||||
commission = COALESCE(@commission, commission),
|
||||
net_proceeds = COALESCE(@netProceeds, net_proceeds),
|
||||
execution_timestamp = COALESCE(@executionTimestamp, execution_timestamp),
|
||||
settlement_timestamp = COALESCE(@settlementTimestamp, settlement_timestamp),
|
||||
kis_response = COALESCE(@kisResponse::jsonb, kis_response),
|
||||
error_message = COALESCE(@errorMessage, error_message),
|
||||
revision = revision + 1
|
||||
WHERE id = @tradeId
|
||||
""";
|
||||
|
||||
await connection.ExecuteAsync(sql, new
|
||||
{
|
||||
id = Guid.NewGuid(),
|
||||
tradeId = trade.Id,
|
||||
newStatus = trade.Status.ToString(),
|
||||
kisOrderId = trade.KisOrderId,
|
||||
executedQuantity = trade.ExecutedQuantity,
|
||||
unitPrice = trade.UnitPrice,
|
||||
totalAmount = trade.TotalAmount,
|
||||
commission = trade.Commission,
|
||||
netProceeds = trade.NetProceeds,
|
||||
executionTimestamp = trade.ExecutionTimestamp,
|
||||
settlementTimestamp = trade.SettlementTimestamp,
|
||||
kisResponse = kisResponse?.ToString(),
|
||||
errorMessage,
|
||||
correlationId = trade.CorrelationId
|
||||
});
|
||||
await connection.ExecuteAsync(UpdateTradeStatusSql, BuildUpdateTradeStatusParams(trade, kisResponse, errorMessage));
|
||||
|
||||
_logger.LogInformation("Updated trade {TradeId} status to {Status}", trade.Id, trade.Status);
|
||||
}
|
||||
|
||||
public async Task UpdateTradeStatusAsync(
|
||||
DbConnection connection,
|
||||
DbTransaction transaction,
|
||||
Trade trade,
|
||||
JsonElement? kisResponse,
|
||||
string? errorMessage,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
await connection.ExecuteAsync(new CommandDefinition(
|
||||
UpdateTradeStatusSql,
|
||||
BuildUpdateTradeStatusParams(trade, kisResponse, errorMessage),
|
||||
transaction,
|
||||
cancellationToken: ct));
|
||||
|
||||
_logger.LogInformation("Updated trade {TradeId} status to {Status} (transactional)", trade.Id, trade.Status);
|
||||
}
|
||||
|
||||
public async Task<int> CountTradesByStatusAsync(TradeStatus status, CancellationToken ct = default)
|
||||
{
|
||||
using var connection = await _dataSource.OpenConnectionAsync(ct);
|
||||
|
||||
Reference in New Issue
Block a user