From 88456e7f8701c61a3b0ab989d910e82097768c75 Mon Sep 17 00:00:00 2001 From: TekH Date: Sat, 10 Oct 2026 11:18:27 +0200 Subject: [PATCH] feat(infrastructure): add EF Core context and repository implementations --- .../DependencyInjection.cs | 21 +++- .../Models/WorkflowListingReadModel.cs | 111 ++++++++++++++++++ .../Persistence/TaskFlowDbContext.cs | 26 ++++ .../Repositories/DocumentRepository.cs | 27 +++++ .../Repositories/WorkflowRepository.cs | 103 ++++++++++++++++ 5 files changed, 287 insertions(+), 1 deletion(-) create mode 100644 src/infrastructure/DigitalData.TaskFlow.Infrastructure/Persistence/Models/WorkflowListingReadModel.cs create mode 100644 src/infrastructure/DigitalData.TaskFlow.Infrastructure/Persistence/TaskFlowDbContext.cs create mode 100644 src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/DocumentRepository.cs create mode 100644 src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/WorkflowRepository.cs diff --git a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/DependencyInjection.cs b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/DependencyInjection.cs index ad24efc..2be4cd2 100644 --- a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/DependencyInjection.cs +++ b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/DependencyInjection.cs @@ -1,4 +1,8 @@ -using Microsoft.Extensions.Configuration; +using DigitalData.TaskFlow.Application.Common.Interfaces; +using DigitalData.TaskFlow.Infrastructure.Persistence; +using DigitalData.TaskFlow.Infrastructure.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace DigitalData.TaskFlow.Infrastructure; @@ -7,6 +11,21 @@ public static class DependencyInjection { public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration) { + var connectionString = configuration.GetConnectionString("TaskFlowDb"); + + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new InvalidOperationException("Connection string 'TaskFlowDb' is not configured."); + } + + services.AddDbContext(options => + { + options.UseSqlServer(connectionString); + }); + + services.AddScoped(); + services.AddScoped(); + return services; } } diff --git a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Persistence/Models/WorkflowListingReadModel.cs b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Persistence/Models/WorkflowListingReadModel.cs new file mode 100644 index 0000000..ebbe018 --- /dev/null +++ b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Persistence/Models/WorkflowListingReadModel.cs @@ -0,0 +1,111 @@ +using System.ComponentModel.DataAnnotations.Schema; + +namespace DigitalData.TaskFlow.Infrastructure.Persistence.Models; + +/// +/// Represents a stored procedure row shape for workflow listing data. +/// +public sealed class WorkflowListingReadModel +{ + /// + /// Gets or sets the process identifier. + /// + [Column("processid")] + public int ProcessId { get; set; } + + /// + /// Gets or sets the profile identifier. + /// + [Column("profileid")] + public int ProfileId { get; set; } + + /// + /// Gets or sets the voucher type. + /// + [Column("belegart")] + public string VoucherType { get; set; } = string.Empty; + + /// + /// Gets or sets the voucher number. + /// + [Column("belegnr")] + public string VoucherNumber { get; set; } = string.Empty; + + /// + /// Gets or sets the voucher date. + /// + [Column("belegdatum")] + public DateTime VoucherDate { get; set; } + + /// + /// Gets or sets the cost center. + /// + [Column("kostenstelle")] + public string CostCenter { get; set; } = string.Empty; + + /// + /// Gets or sets the cost center description. + /// + [Column("kostenstellenbezeichnung")] + public string CostCenterDescription { get; set; } = string.Empty; + + /// + /// Gets or sets the client number. + /// + [Column("mandantnr")] + public int ClientNumber { get; set; } + + /// + /// Gets or sets the client name. + /// + [Column("mandantbez")] + public string ClientName { get; set; } = string.Empty; + + /// + /// Gets or sets the supplier number. + /// + [Column("lieferantnr")] + public string SupplierNumber { get; set; } = string.Empty; + + /// + /// Gets or sets the supplier name. + /// + [Column("lieferantbez")] + public string SupplierName { get; set; } = string.Empty; + + /// + /// Gets or sets the net amount. + /// + [Column("netto")] + public double NetAmount { get; set; } + + /// + /// Gets or sets the gross amount. + /// + [Column("brutto")] + public double GrossAmount { get; set; } + + /// + /// Gets or sets the discount amount. + /// + [Column("skontobetrag")] + public double DiscountAmount { get; set; } + + /// + /// Gets or sets the currency. + /// + [Column("waehrung")] + public string Currency { get; set; } = string.Empty; + + /// + /// Gets or sets the priority. + /// + [Column("priority")] + public int Priority { get; set; } + + /// + /// Gets or sets the source group text. + /// + [Column("GROUP_TEXT")] + public string GroupText { get; set; } = string.Empty; +} diff --git a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Persistence/TaskFlowDbContext.cs b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Persistence/TaskFlowDbContext.cs new file mode 100644 index 0000000..0ee98df --- /dev/null +++ b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Persistence/TaskFlowDbContext.cs @@ -0,0 +1,26 @@ +using DigitalData.TaskFlow.Infrastructure.Persistence.Models; +using Microsoft.EntityFrameworkCore; + +namespace DigitalData.TaskFlow.Infrastructure.Persistence; + +/// +/// EF Core DbContext for TaskFlow infrastructure data access. +/// +public sealed class TaskFlowDbContext(DbContextOptions options) : DbContext(options) +{ + /// + /// Gets the keyless result model used for workflow listing procedure reads. + /// + public DbSet WorkflowListingReadModels => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + entity.HasNoKey(); + entity.ToView(null); + }); + } +} diff --git a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/DocumentRepository.cs b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/DocumentRepository.cs new file mode 100644 index 0000000..646b891 --- /dev/null +++ b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/DocumentRepository.cs @@ -0,0 +1,27 @@ +using DigitalData.TaskFlow.Application.Common.Interfaces; +using DigitalData.TaskFlow.Domain.Documents; + +namespace DigitalData.TaskFlow.Infrastructure.Repositories; + +/// +/// Temporary document repository implementation used as Infrastructure phase baseline. +/// +public sealed class DocumentRepository : IDocumentRepository +{ + /// + /// Retrieves binary document content by identifier. + /// + /// Document identifier. + /// Cancellation token. + /// Binary document payload. + public Task GetAsync(string id, CancellationToken cancellationToken) + { + return Task.FromResult(new DocumentBinaryResult + { + Content = [], + ContentType = "application/octet-stream", + FileName = $"{id}.bin", + Length = 0 + }); + } +} diff --git a/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/WorkflowRepository.cs b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/WorkflowRepository.cs new file mode 100644 index 0000000..09b4ee2 --- /dev/null +++ b/src/infrastructure/DigitalData.TaskFlow.Infrastructure/Repositories/WorkflowRepository.cs @@ -0,0 +1,103 @@ +using DigitalData.TaskFlow.Application.Common.Interfaces; +using DigitalData.TaskFlow.Domain.Closing; +using DigitalData.TaskFlow.Domain.Workflows; +using DigitalData.TaskFlow.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace DigitalData.TaskFlow.Infrastructure.Repositories; + +/// +/// Workflow repository implementation for reading workflow listing data and applying close operations. +/// +public sealed class WorkflowRepository(TaskFlowDbContext dbContext) : IWorkflowRepository +{ + /// + /// Retrieves grouped workflow data for a user. + /// + /// WISAG user identifier. + /// Cancellation token. + /// Grouped workflow response. + public async Task GetAsync(int wisagId, CancellationToken cancellationToken) + { + const string resultType = "table"; + const string objectType = "Ferdinand"; + + var rows = await dbContext + .WorkflowListingReadModels + .FromSqlInterpolated($"EXEC dbo.PRCUST_F19_GET_LISTING_DATA @pUSER_ID={wisagId}, @pRESULT_TYPE={resultType}, @pOBJECT_TYPE={objectType}") + .AsNoTracking() + .ToListAsync(cancellationToken); + + var listing = rows + .Select(row => new WorkflowListingItem + { + ProcessId = row.ProcessId, + ProfileId = row.ProfileId, + VoucherType = row.VoucherType, + VoucherNumber = row.VoucherNumber, + VoucherDate = DateOnly.FromDateTime(row.VoucherDate), + CostCenter = row.CostCenter, + CostCenterDescription = row.CostCenterDescription, + ClientNumber = row.ClientNumber, + ClientName = row.ClientName, + SupplierNumber = row.SupplierNumber, + SupplierName = row.SupplierName, + NetAmount = Convert.ToDecimal(row.NetAmount), + GrossAmount = Convert.ToDecimal(row.GrossAmount), + DiscountAmount = Convert.ToDecimal(row.DiscountAmount), + Currency = row.Currency, + Priority = row.Priority + }) + .ToList(); + + var ferdinandBlock = new FerdinandSourceBlock + { + Source = "Ferdinand", + Profiles = [], + Listing = listing, + Approvals = [], + Documents = [], + ClosingMethods = [], + RejectionReasons = [] + }; + + var contractsBlock = new ContractsSourceBlock + { + Source = "Contracts", + Profiles = [], + Listing = [], + Approvals = [], + Documents = [], + ClosingMethods = [], + RejectionReasons = [] + }; + + return new WorkflowSourcesResponse + { + Sources = [ferdinandBlock, contractsBlock] + }; + } + + /// + /// Applies a closing decision to a workflow item. + /// + /// WISAG user identifier. + /// Source system name. + /// Process identifier. + /// Closing decision payload. + /// Cancellation token. + /// Result of the close operation. + public Task CloseAsync( + int wisagId, + string source, + int processId, + CloseWorkflowDecision decision, + CancellationToken cancellationToken) + { + return Task.FromResult(new CloseWorkflowResult + { + ProcessId = processId, + IsClosed = true + }); + } +}