using Dapper; using Npgsql; namespace Shared.Operations; public sealed record UpsertReconcileItem( string TenantId, string ReconcileType, string ReferenceNo, string SourceLabel, string TargetLabel, string ExpectedValue, string ActualValue, string? DifferenceValue, string? ReasonCode, string? ReasonText, string Status, string? SourceId, string? TargetId, DateTimeOffset OccurredAt); public sealed class ReconcileProjectionWriter(NpgsqlDataSource dataSource) { public async Task UpsertAsync(UpsertReconcileItem item, CancellationToken ct) { await using var connection = await dataSource.OpenConnectionAsync(ct); var id = Guid.NewGuid(); const string sql = """ insert into kbx.reconcile_items( id, tenant_id, reconcile_type, reference_no, source_label, target_label, expected_value, actual_value, difference_value, reason_code, reason_text, status, source_id, target_id, identity_key, occurred_at) values( @Id, @TenantId, @ReconcileType, @ReferenceNo, @SourceLabel, @TargetLabel, @ExpectedValue, @ActualValue, @DifferenceValue, @ReasonCode, @ReasonText, @Status, @SourceId, @TargetId, @IdentityKey, @OccurredAt) on conflict(tenant_id, reconcile_type, reference_no, identity_key) do update set source_label=excluded.source_label, target_label=excluded.target_label, expected_value=excluded.expected_value, actual_value=excluded.actual_value, difference_value=excluded.difference_value, reason_code=excluded.reason_code, reason_text=excluded.reason_text, status=excluded.status, occurred_at=excluded.occurred_at, version=kbx.reconcile_items.version+1, updated_at=now() where excluded.occurred_at >= kbx.reconcile_items.occurred_at returning id; """; var args = new { Id = id, item.TenantId, item.ReconcileType, item.ReferenceNo, item.SourceLabel, item.TargetLabel, item.ExpectedValue, item.ActualValue, item.DifferenceValue, item.ReasonCode, item.ReasonText, item.Status, item.SourceId, item.TargetId, IdentityKey = $"{item.SourceId ?? "-"}|{item.TargetId ?? "-"}", item.OccurredAt, }; var changedId = await connection.ExecuteScalarAsync(new CommandDefinition(sql, args, cancellationToken: ct)); if (changedId is not null) return changedId.Value; return await connection.ExecuteScalarAsync(new CommandDefinition(""" select id from kbx.reconcile_items where tenant_id=@TenantId and reconcile_type=@ReconcileType and reference_no=@ReferenceNo and identity_key=@IdentityKey; """, args, cancellationToken: ct)); } }