浏览代码

Projektdateien hinzufügen.

Arne Diekmann 5 年之前
父节点
当前提交
fb1cd72eb7
共有 32 个文件被更改,包括 2105 次插入0 次删除
  1. 25 0
      .dockerignore
  2. 244 0
      GreenTree.Banking.API.comdirect/AppHost.cs
  3. 43 0
      GreenTree.Banking.API.comdirect/Configuration/AuthenticationOptions.cs
  4. 23 0
      GreenTree.Banking.API.comdirect/Configuration/FileSystemExportOptions.cs
  5. 18 0
      GreenTree.Banking.API.comdirect/Configuration/IDataExportOptions.cs
  6. 23 0
      GreenTree.Banking.API.comdirect/Configuration/MySqlExportOptions.cs
  7. 33 0
      GreenTree.Banking.API.comdirect/Configuration/SessionOptions.cs
  8. 18 0
      GreenTree.Banking.API.comdirect/Data/Depot/AdditionalData.cs
  9. 53 0
      GreenTree.Banking.API.comdirect/Data/Depot/Aggregated.cs
  10. 23 0
      GreenTree.Banking.API.comdirect/Data/Depot/CurrentValue.cs
  11. 23 0
      GreenTree.Banking.API.comdirect/Data/Depot/Depot.cs
  12. 48 0
      GreenTree.Banking.API.comdirect/Data/Depot/Instrument.cs
  13. 25 0
      GreenTree.Banking.API.comdirect/Data/Depot/Price.cs
  14. 18 0
      GreenTree.Banking.API.comdirect/Data/Depot/StaticData.cs
  15. 13 0
      GreenTree.Banking.API.comdirect/Data/Depot/Unit.cs
  16. 103 0
      GreenTree.Banking.API.comdirect/Data/Depot/Value.cs
  17. 20 0
      GreenTree.Banking.API.comdirect/Data/Depot/ValueInstrument.cs
  18. 58 0
      GreenTree.Banking.API.comdirect/Data/OAuth2Response.cs
  19. 28 0
      GreenTree.Banking.API.comdirect/Data/SessionResponse.cs
  20. 28 0
      GreenTree.Banking.API.comdirect/Data/SessionTANResponse.cs
  21. 20 0
      GreenTree.Banking.API.comdirect/Dockerfile
  22. 25 0
      GreenTree.Banking.API.comdirect/Extension/MySqlCommandExtension.cs
  23. 25 0
      GreenTree.Banking.API.comdirect/Extension/RestRequestExtension.cs
  24. 26 0
      GreenTree.Banking.API.comdirect/GreenTree.Banking.API.comdirect.csproj
  25. 48 0
      GreenTree.Banking.API.comdirect/Program.cs
  26. 10 0
      GreenTree.Banking.API.comdirect/Properties/launchSettings.json
  27. 97 0
      GreenTree.Banking.API.comdirect/Services/FileSystemDataExportService.cs
  28. 42 0
      GreenTree.Banking.API.comdirect/Services/IDataExportService.cs
  29. 400 0
      GreenTree.Banking.API.comdirect/Services/MySqlDataExportService.cs
  30. 496 0
      GreenTree.Banking.API.comdirect/Web/Session.cs
  31. 24 0
      GreenTree.Banking.API.comdirect/appsettings.json
  32. 25 0
      GreenTree.Banking.sln

+ 25 - 0
.dockerignore

@@ -0,0 +1,25 @@
+**/.classpath
+**/.dockerignore
+**/.env
+**/.git
+**/.gitignore
+**/.project
+**/.settings
+**/.toolstarget
+**/.vs
+**/.vscode
+**/*.*proj.user
+**/*.dbmdl
+**/*.jfm
+**/azds.yaml
+**/bin
+**/charts
+**/docker-compose*
+**/Dockerfile*
+**/node_modules
+**/npm-debug.log
+**/obj
+**/secrets.dev.yaml
+**/values.dev.yaml
+LICENSE
+README.md

+ 244 - 0
GreenTree.Banking.API.comdirect/AppHost.cs

@@ -0,0 +1,244 @@
+using GreenTree.Banking.API.comdirect.Configuration;
+using GreenTree.Banking.API.comdirect.Services;
+using GreenTree.Banking.API.comdirect.Web;
+using Microsoft.Extensions.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+
+namespace GreenTree.Banking.API.comdirect
+{
+    public class AppHost
+    {
+        #region DI fields
+
+        // Web API session
+        private readonly Session _session;
+
+        // The global session options
+        private readonly SessionOptions _sessionOptions;
+
+        // The global data export services
+        private readonly IEnumerable<IDataExportService> _dataExportServices;
+
+        #endregion
+
+        #region Fields
+
+        // Data fetch timer
+        private Timer fetchDataTimer;
+
+        // Data additional fetch timer
+        private Timer fetchAdditionalDataTimer;
+
+        // Refresh session timer
+        private Timer refreshSessionTimer;
+
+        // Set the cancellation to true if the application should be closed
+        private bool cancellationToken = false;
+
+        // Count of occured errors
+        private int errorCounter = 0;
+
+        #endregion
+
+        #region Ctor
+
+        /// <summary>
+        /// Initializes a new instance of the AppHost class
+        /// </summary>
+        /// <param name="session">Global Web-API session.</param>
+        /// <param name="sessionOptions">The global session options.</param>
+        /// <param name="dataExportServices">The global data export services.</param>
+        public AppHost(
+            Session session,
+            SessionOptions sessionOptions,
+            IEnumerable<IDataExportService> dataExportServices)
+        {
+            _session = session;
+            _sessionOptions = sessionOptions;
+            _dataExportServices = dataExportServices;
+        }
+
+        #endregion
+
+        #region Application
+
+        /// <summary>
+        /// Run application
+        /// </summary>
+        public void Run()
+        {
+            Console.WriteLine("Application running...");
+
+            Initiate();
+        }
+
+        /// <summary>
+        /// Initiate API session
+        /// </summary>
+        private void Initiate()
+        {
+            Console.WriteLine("Starting to authenticate...");
+
+            try
+            {
+                _session.Authenticate();
+
+                Console.WriteLine("Authentication successful!");
+                Console.WriteLine("Fetching depot data...");
+
+                _session.GetDefaultDepot();
+
+                Console.WriteLine("Fetching data successful!");
+
+                Console.WriteLine("Setting up data fetch timer (interval: every {0} minutes)...", _sessionOptions.FetchDataInterval);
+
+                fetchDataTimer = new Timer(
+                    new TimerCallback(FetchData), null, 60000, _sessionOptions.FetchDataInterval * 60000);
+
+                fetchAdditionalDataTimer = new Timer(
+                    new TimerCallback(FetchAdditionalData), null, 120000, _sessionOptions.FetchDataInterval * 60000);
+
+                Console.WriteLine("Setting up session refresh timer (interval: every {0} minutes)...", _sessionOptions.RefreshSessionInterval);
+
+                refreshSessionTimer = new Timer(
+                    new TimerCallback(RefreshSession), null, _sessionOptions.RefreshSessionInterval * 60000, _sessionOptions.RefreshSessionInterval * 60000);
+            }
+            catch (Exception ex)
+            {
+                Console.Write("Error while application startup:");
+                Console.Write(ex.Message);
+
+                errorCounter++;
+
+                Console.Write("Error count: {0}", errorCounter);
+
+                if (errorCounter >= 3)
+                    cancellationToken = true;
+                else
+                    Initiate();
+            }
+
+            while (!cancellationToken)
+            {
+                Thread.Sleep(1000);
+            }
+
+            Console.Write("Exceeded 3 errors... exiting application!");
+        }
+
+        /// <summary>
+        /// Fetch data from the current session
+        /// </summary>
+        /// <param name="state">Dummy parameter.</param>
+        private void FetchData(object state)
+        {
+            try
+            {
+                Console.WriteLine("Fetching depot portfolio data...");
+
+                var data = _session.GetDepotPortfolioData();
+
+                foreach (var exportService in _dataExportServices)
+                {
+                    if (exportService.Activated)
+                        exportService.ExportData(data);
+                }
+
+                errorCounter = 0;
+
+                Console.WriteLine("Fetching depot portfolio data successful!");
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine("Error fetching data - Canceling fetch data timer:");
+                Console.Write(ex.Message);
+
+                fetchDataTimer.Change(Timeout.Infinite, Timeout.Infinite);
+
+                errorCounter++;
+
+                Console.Write("Error count: {0}", errorCounter);
+
+                if (errorCounter >= 3)
+                    cancellationToken = true;
+                else
+                    Initiate();
+            }
+        }
+
+        /// <summary>
+        /// Fetch additional data from the current session
+        /// </summary>
+        /// <param name="state">Dummy parameter.</param>
+        private void FetchAdditionalData(object state)
+        {
+            try
+            {
+                fetchAdditionalDataTimer.Change(Timeout.Infinite, Timeout.Infinite);
+
+                Console.WriteLine("Fetching depot additional data...");
+
+                var data = _session.GetDepotRevenueData();
+
+                foreach (var exportService in _dataExportServices)
+                {
+                    if (exportService.Activated)
+                        exportService.ExportAdditionalData(data);
+                }
+
+                errorCounter = 0;
+
+                Console.WriteLine("Fetching depot additional data successful!");
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine("Error fetching additional data - Canceling fetch additional data timer:");
+                Console.Write(ex.Message);
+
+                errorCounter++;
+
+                Console.Write("Error count: {0}", errorCounter);
+
+                if (errorCounter >= 3)
+                    cancellationToken = true;
+                else
+                    Initiate();
+            }
+        }
+
+        /// <summary>
+        /// Refresh the current session
+        /// </summary>
+        /// <param name="state">Dummy parameter.</param>
+        private void RefreshSession(object state)
+        {
+            try
+            {
+                _session.RefreshAuthentication();
+
+                errorCounter = 0;
+            }
+            catch (Exception ex)
+            {
+                Console.WriteLine("Error refreshing session - Canceling session refresh timer:");
+                Console.Write(ex.Message);
+
+                refreshSessionTimer.Change(Timeout.Infinite, Timeout.Infinite);
+
+                errorCounter++;
+
+                Console.Write("Error count: {0}", errorCounter);
+
+                if (errorCounter >= 3)
+                    cancellationToken = true;
+                else
+                    Initiate();
+            }
+        }
+
+        #endregion
+    }
+}

+ 43 - 0
GreenTree.Banking.API.comdirect/Configuration/AuthenticationOptions.cs

@@ -0,0 +1,43 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Configuration
+{
+    public class AuthenticationOptions
+    {
+        #region Properties
+
+        /// <summary>
+        /// The base URL of the API
+        /// </summary>
+        public string BaseUrl { get; set; }
+
+        /// <summary>
+        /// The OAuth URL for authentication
+        /// </summary>
+        public string OAuthUrl { get; set; }
+
+        /// <summary>
+        /// The API client id
+        /// </summary>
+        public string ClientId { get; set; }
+
+        /// <summary>
+        /// The API client secret
+        /// </summary>
+        public string ClientSecret { get; set; }
+
+        /// <summary>
+        /// The login number for the account
+        /// </summary>
+        public string LoginNumber { get; set; }
+
+        /// <summary>
+        /// The login PIN for the account
+        /// </summary>
+        public string LoginPin { get; set; }
+
+        #endregion
+    }
+}

+ 23 - 0
GreenTree.Banking.API.comdirect/Configuration/FileSystemExportOptions.cs

@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Configuration
+{
+    public class FileSystemExportOptions : IDataExportOptions
+    {
+        #region Properties
+
+        /// <summary>
+        /// Activation status
+        /// </summary>
+        public bool Activated { get; set; }
+
+        /// <summary>
+        /// The path where the data shall be exported
+        /// </summary>
+        public string Path { get; set; }
+
+        #endregion
+    }
+}

+ 18 - 0
GreenTree.Banking.API.comdirect/Configuration/IDataExportOptions.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Configuration
+{
+    public interface IDataExportOptions
+    {
+        #region Properties
+
+        /// <summary>
+        /// Activation status
+        /// </summary>
+        public bool Activated { get; set; }
+
+        #endregion
+    }
+}

+ 23 - 0
GreenTree.Banking.API.comdirect/Configuration/MySqlExportOptions.cs

@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Configuration
+{
+    public class MySqlExportOptions : IDataExportOptions
+    {
+        #region Properties
+
+        /// <summary>
+        /// Activation status
+        /// </summary>
+        public bool Activated { get; set; }
+
+        /// <summary>
+        /// Database connection string
+        /// </summary>
+        public string ConnectionString { get; set; }
+
+        #endregion
+    }
+}

+ 33 - 0
GreenTree.Banking.API.comdirect/Configuration/SessionOptions.cs

@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Configuration
+{
+    public class SessionOptions
+    {
+        #region Properties
+
+        /// <summary>
+        /// The seconds to wait before the session proceeds the authentcation after the initiation of the TAN validation
+        /// </summary>
+        public int TimeoutForTwoFactorTANAuthentication { get; set; }
+
+        /// <summary>
+        /// The miliseconds to wait for the REST client to timeout after a new request
+        /// </summary>
+        public int RestTimeout { get; set; }
+
+        /// <summary>
+        /// Interval for timer which fetches the data regularly
+        /// </summary>
+        public int FetchDataInterval { get; set; }
+
+        /// <summary>
+        /// Interval for timer which refreshes the session
+        /// </summary>
+        public int RefreshSessionInterval { get; set; }
+
+        #endregion
+    }
+}

+ 18 - 0
GreenTree.Banking.API.comdirect/Data/Depot/AdditionalData.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data.Depot
+{
+    public class AdditionalData
+    {
+        #region Properties
+
+        /// <summary>
+        /// Values
+        /// </summary>
+        public ValueInstrument[] Values { get; set; }
+
+        #endregion
+    }
+}

+ 53 - 0
GreenTree.Banking.API.comdirect/Data/Depot/Aggregated.cs

@@ -0,0 +1,53 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data.Depot
+{
+    public class Aggregated
+    {
+        #region Properties
+
+        /// <summary>
+        /// The depot
+        /// </summary>
+        public Depot Depot { get; set; }
+
+        /// <summary>
+        /// Value of yesterday
+        /// </summary>
+        public CurrentValue PrevDayValue { get; set; }
+
+        /// <summary>
+        /// Current value
+        /// </summary>
+        public CurrentValue CurrentValue { get; set; }
+
+        /// <summary>
+        /// Purchase value
+        /// </summary>
+        public CurrentValue PurchaseValue { get; set; }
+
+        /// <summary>
+        /// Absolute Loss / profit compared to to buying
+        /// </summary>
+        public CurrentValue ProfitLossPurchaseAbs { get; set; }
+
+        /// <summary>
+        /// Relative Loss / profit compared to buying
+        /// </summary>
+        public string ProfitLossPurchaseRel { get; set; }
+
+        /// <summary>
+        /// Absolute Loss / profit compared to yesterday
+        /// </summary>
+        public CurrentValue ProfitLossPrevDayAbs { get; set; }
+
+        /// <summary>
+        /// Relative Loss / profit compared to yesterday
+        /// </summary>
+        public string ProfitLossPrevDayRel { get; set; }
+
+        #endregion
+    }
+}

+ 23 - 0
GreenTree.Banking.API.comdirect/Data/Depot/CurrentValue.cs

@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data.Depot
+{
+    public class CurrentValue
+    {
+        #region Properties
+
+        /// <summary>
+        /// Actual value
+        /// </summary>
+        public string Value { get; set; }
+
+        /// <summary>
+        /// Value unit
+        /// </summary>
+        public Unit Unit { get; set; }
+
+        #endregion
+    }
+}

+ 23 - 0
GreenTree.Banking.API.comdirect/Data/Depot/Depot.cs

@@ -0,0 +1,23 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data.Depot
+{
+    public class Depot
+    {
+        #region Properties
+
+        /// <summary>
+        /// Aggregation
+        /// </summary>
+        public Aggregated Aggregated { get; set; }
+
+        /// <summary>
+        /// Values
+        /// </summary>
+        public Value[] Values { get; set; }
+
+        #endregion
+    }
+}

+ 48 - 0
GreenTree.Banking.API.comdirect/Data/Depot/Instrument.cs

@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data.Depot
+{
+    public class Instrument
+    {
+        #region Properties
+
+        /// <summary>
+        /// Instrument id
+        /// </summary>
+        public string InstrumentId { get; set; }
+
+        /// <summary>
+        /// WKN number
+        /// </summary>
+        public string Wkn { get; set; }
+
+        /// <summary>
+        /// ISIN number
+        /// </summary>
+        public string Isin { get; set; }
+
+        /// <summary>
+        /// Mnemomic shortance
+        /// </summary>
+        public string Mnemonic { get; set; }
+
+        /// <summary>
+        /// Full name
+        /// </summary>
+        public string Name { get; set; }
+
+        /// <summary>
+        /// Short name
+        /// </summary>
+        public string ShortName { get; set; }
+
+        /// <summary>
+        /// Static type data
+        /// </summary>
+        public StaticData StaticData { get; set; }
+
+        #endregion
+    }
+}

+ 25 - 0
GreenTree.Banking.API.comdirect/Data/Depot/Price.cs

@@ -0,0 +1,25 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data.Depot
+{
+    public class Price
+    {
+        #region Properties
+
+        /// <summary>
+        /// Price value
+        /// </summary>
+        [JsonProperty("Price")]
+        public CurrentValue PriceValue { get; set; }
+
+        /// <summary>
+        /// Timestamp of value
+        /// </summary>
+        public string PriceDateTime { get; set; }
+
+        #endregion
+    }
+}

+ 18 - 0
GreenTree.Banking.API.comdirect/Data/Depot/StaticData.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data.Depot
+{
+    public class StaticData
+    {
+        #region Properties
+
+        /// <summary>
+        /// Instrument type
+        /// </summary>
+        public string InstrumentType { get; set; }
+
+        #endregion
+    }
+}

+ 13 - 0
GreenTree.Banking.API.comdirect/Data/Depot/Unit.cs

@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data.Depot
+{
+    public enum Unit 
+    { 
+        Eur,
+        Usd,
+        Xxx
+    };
+}

+ 103 - 0
GreenTree.Banking.API.comdirect/Data/Depot/Value.cs

@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data.Depot
+{
+    public class Value
+    {
+        #region Properties
+
+        /// <summary>
+        /// Depot identifier
+        /// </summary>
+        public string DepotId { get; set; }
+
+        /// <summary>
+        /// Position identifier
+        /// </summary>
+        public string PositionId { get; set; }
+
+        /// <summary>
+        /// Value paper identifier
+        /// </summary>
+        public string Wkn { get; set; }
+
+        /// <summary>
+        /// Custody type
+        /// </summary>
+        public string CustodyType { get; set; }
+
+        /// <summary>
+        /// Quantity
+        /// </summary>
+        public CurrentValue Quantity { get; set; }
+
+        /// <summary>
+        /// Available quantity
+        /// </summary>
+        public CurrentValue AvailableQuantity { get; set; }
+
+        /// <summary>
+        /// Current price / value
+        /// </summary>
+        public Price CurrentPrice { get; set; }
+
+        /// <summary>
+        /// Purchase price / value
+        /// </summary>
+        public CurrentValue PurchasePrice { get; set; }
+
+        /// <summary>
+        /// Price /value yesterday
+        /// </summary>
+        public Price PrevDayPrice { get; set; }
+
+        /// <summary>
+        /// Current value
+        /// </summary>
+        public CurrentValue CurrentValue { get; set; }
+
+        /// <summary>
+        /// Purchase value
+        /// </summary>
+        public CurrentValue PurchaseValue { get; set; }
+
+        /// <summary>
+        /// Absolute profit / loss to purchase
+        /// </summary>
+        public CurrentValue ProfitLossPurchaseAbs { get; set; }
+
+        /// <summary>
+        /// Relative profit / loss to yesterday
+        /// </summary>
+        public string ProfitLossPurchaseRel { get; set; }
+
+        /// <summary>
+        /// Absolute profit / loss to purchase
+        /// </summary>
+        public CurrentValue ProfitLossPrevDayAbs { get; set; }
+
+        /// <summary>
+        /// Relative profit / loss to yesterday
+        /// </summary>
+        public string ProfitLossPrevDayRel { get; set; }
+
+        /// <summary>
+        /// Version
+        /// </summary>
+        public object Version { get; set; }
+
+        /// <summary>
+        /// Hedgeability status
+        /// </summary>
+        public string Hedgeability { get; set; }
+
+        /// <summary>
+        /// Hedgeability quantity
+        /// </summary>
+        public CurrentValue AvailableQuantityToHedge { get; set; }
+
+        #endregion
+    }
+}

+ 20 - 0
GreenTree.Banking.API.comdirect/Data/Depot/ValueInstrument.cs

@@ -0,0 +1,20 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data.Depot
+{
+    public class ValueInstrument
+    {
+        #region Properties
+
+        /// <summary>
+        /// Instrument
+        /// </summary>
+        [JsonProperty("instrument")]
+        public Instrument Instrument { get; set; }
+
+        #endregion
+    }
+}

+ 58 - 0
GreenTree.Banking.API.comdirect/Data/OAuth2Response.cs

@@ -0,0 +1,58 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data
+{
+    public class OAuth2Response
+    {
+        #region Properties
+
+        /// <summary>
+        /// OAuth2 access token GUID
+        /// </summary>
+        [JsonProperty("access_token")]
+        public Guid AccessToken { get; set; }
+
+        /// <summary>
+        /// The token type
+        /// </summary>
+        [JsonProperty("token_type")]
+        public string TokenType { get; set; }
+
+        /// <summary>
+        /// Refresh token when refreshing the session
+        /// </summary>
+        [JsonProperty("refresh_token")]
+        public Guid RefreshToken { get; set; }
+
+        /// <summary>
+        /// Seconds when the session is going to expire
+        /// </summary>
+        [JsonProperty("expires_in")]
+        public long ExpiresIn { get; set; }
+
+        /// <summary>
+        /// Current authentication scope
+        /// </summary>
+        public string Scope { get; set; }
+
+        /// <summary>
+        /// Customer ID
+        /// </summary>
+        public string Kdnr { get; set; }
+
+        /// <summary>
+        /// Businesspartner ID
+        /// </summary>
+        public long Bpid { get; set; }
+
+        /// <summary>
+        /// Contact ID
+        /// </summary>
+        public long KontaktId { get; set; }
+
+        #endregion
+    }
+}

+ 28 - 0
GreenTree.Banking.API.comdirect/Data/SessionResponse.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data
+{
+    public class SessionResponse
+    {
+        #region Properties
+
+        /// <summary>
+        /// API identifier value
+        /// </summary>
+        public string Identifier { get; set; }
+
+        /// <summary>
+        /// Status of the TAN activation
+        /// </summary>
+        public bool SessionTanActive { get; set; }
+
+        /// <summary>
+        /// Status wether two-factor authentication is enabled
+        /// </summary>
+        public bool Activated2Fa { get; set; }
+
+        #endregion
+    }
+}

+ 28 - 0
GreenTree.Banking.API.comdirect/Data/SessionTANResponse.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Data
+{
+    public class SessionTANResponse
+    {
+        #region Properties
+
+        /// <summary>
+        /// Session TAN identifier
+        /// </summary>
+        public long Id { get; set; }
+
+        /// <summary>
+        /// TAN type
+        /// </summary>
+        public string Typ { get; set; }
+
+        /// <summary>
+        /// Available TAN types
+        /// </summary>
+        public string[] AvailableTypes { get; set; }
+
+        #endregion
+    }
+}

+ 20 - 0
GreenTree.Banking.API.comdirect/Dockerfile

@@ -0,0 +1,20 @@
+#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
+
+FROM mcr.microsoft.com/dotnet/core/runtime:3.1-buster-slim AS base
+WORKDIR /app
+
+FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build
+WORKDIR /src
+COPY ["GreenTree.Banking.API.comdirect/GreenTree.Banking.API.comdirect.csproj", "GreenTree.Banking.API.comdirect/"]
+RUN dotnet restore "GreenTree.Banking.API.comdirect/GreenTree.Banking.API.comdirect.csproj"
+COPY . .
+WORKDIR "/src/GreenTree.Banking.API.comdirect"
+RUN dotnet build "GreenTree.Banking.API.comdirect.csproj" -c Release -o /app/build
+
+FROM build AS publish
+RUN dotnet publish "GreenTree.Banking.API.comdirect.csproj" -c Release -o /app/publish
+
+FROM base AS final
+WORKDIR /app
+COPY --from=publish /app/publish .
+ENTRYPOINT ["dotnet", "GreenTree.Banking.API.comdirect.dll"]

+ 25 - 0
GreenTree.Banking.API.comdirect/Extension/MySqlCommandExtension.cs

@@ -0,0 +1,25 @@
+using MySqlConnector;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Extension
+{
+    public static class MySqlCommandExtension
+    {
+        /// <summary>
+        /// Execute command and return first row of the resulting data set
+        /// </summary>
+        /// <param name="command">The SQL command.</param>
+        /// <returns>First data row in the dataset.</returns>
+        public static object[] ExecuteFirst(this MySqlCommand command)
+        {
+            var table = new DataTable();
+
+            table.Load(command.ExecuteReader());
+
+            return table.Rows.Count > 0 ? table.Rows[0].ItemArray : null;
+        }
+    }
+}

+ 25 - 0
GreenTree.Banking.API.comdirect/Extension/RestRequestExtension.cs

@@ -0,0 +1,25 @@
+using RestSharp;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Extension
+{
+    public static class RestRequestExtension
+    {
+        /// <summary>
+        /// Add multiple cookies to a REST request at once
+        /// </summary>
+        /// <param name="request">The REST request.</param>
+        /// <param name="cookies">The cookies.</param>
+        public static void AddCookies(this RestRequest request, IList<RestResponseCookie> cookies)
+        {
+            if (cookies == null) return;
+
+            foreach (var cookie in cookies)
+            {
+                request.AddCookie(cookie.Name, cookie.Value);
+            }
+        }
+    }
+}

+ 26 - 0
GreenTree.Banking.API.comdirect/GreenTree.Banking.API.comdirect.csproj

@@ -0,0 +1,26 @@
+<Project Sdk="Microsoft.NET.Sdk;Microsoft.NET.Sdk.Publish">
+  <PropertyGroup>
+    <OutputType>Exe</OutputType>
+    <TargetFramework>netcoreapp3.1</TargetFramework>
+    <DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
+  </PropertyGroup>
+  <ItemGroup>
+    <None Remove="appsettings.json" />
+  </ItemGroup>
+  <ItemGroup>
+    <Content Include="appsettings.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
+  <ItemGroup>
+    <PackageReference Include="Microsoft.Extensions.Configuration" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.9" />
+    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.9" />
+    <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.9" />
+    <PackageReference Include="MySqlConnector" Version="1.0.1" />
+    <PackageReference Include="RestSharp" Version="106.11.7" />
+    <PackageReference Include="RestSharp.Serializers.NewtonsoftJson" Version="106.11.7" />
+  </ItemGroup>
+</Project>

+ 48 - 0
GreenTree.Banking.API.comdirect/Program.cs

@@ -0,0 +1,48 @@
+using GreenTree.Banking.API.comdirect.Configuration;
+using GreenTree.Banking.API.comdirect.Services;
+using GreenTree.Banking.API.comdirect.Web;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.IO;
+
+namespace GreenTree.Banking.API.comdirect
+{
+    class Program
+    {
+        static void Main(string[] args)
+        {
+            // Startup application
+            Console.WriteLine("Starting up applicaton...");
+            Console.WriteLine("Reading configuration 'appsettings.json'... ");
+
+            // Create config builder and read current appsettings.json
+            var builder = new ConfigurationBuilder()
+                .SetBasePath(Directory.GetCurrentDirectory())
+                .AddJsonFile("appsettings.json");
+
+            var config = builder.Build();
+
+            // Get options
+            var authenticationOptions = config.Get<AuthenticationOptions>();
+            var sessionOptions = config.Get<SessionOptions>();
+
+            config.Bind("AuthenticationOptions", authenticationOptions);
+            config.Bind("SessionOptions", sessionOptions);
+
+            // Setup container and register services
+            var serviceProvider = new ServiceCollection()
+                .AddSingleton(typeof(IConfiguration), config)
+                .AddSingleton(typeof(AuthenticationOptions), authenticationOptions)
+                .AddSingleton(typeof(SessionOptions), sessionOptions)
+                .AddSingleton<IDataExportService, FileSystemDataExportService>()
+                .AddSingleton<IDataExportService, MySqlDataExportService>()
+                .AddSingleton<Session, Session>()
+                .AddSingleton<AppHost, AppHost>()
+                .BuildServiceProvider();
+
+            // Run application host
+            serviceProvider.GetService<AppHost>().Run();
+        }
+    }
+}

+ 10 - 0
GreenTree.Banking.API.comdirect/Properties/launchSettings.json

@@ -0,0 +1,10 @@
+{
+  "profiles": {
+    "GreenTree.Banking.API.comdirect": {
+      "commandName": "Project"
+    },
+    "Docker": {
+      "commandName": "Project"
+    }
+  }
+}

+ 97 - 0
GreenTree.Banking.API.comdirect/Services/FileSystemDataExportService.cs

@@ -0,0 +1,97 @@
+using GreenTree.Banking.API.comdirect.Configuration;
+using GreenTree.Banking.API.comdirect.Data;
+using GreenTree.Banking.API.comdirect.Data.Depot;
+using Microsoft.Extensions.Configuration;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Services
+{
+    public class FileSystemDataExportService : IDataExportService
+    {
+        #region DI fields
+
+        // The global configuration
+        private readonly IConfiguration _configuration;
+
+        #endregion
+
+        #region Properties
+
+        /// <summary>
+        /// Activation status
+        /// </summary>
+        public bool Activated
+        {
+            get
+            {
+                return FileSystemExportOptions != null && FileSystemExportOptions.Activated;
+            }
+        }
+
+        /// <summary>
+        /// The appropriate options for the data export
+        /// </summary>
+        public FileSystemExportOptions FileSystemExportOptions { get; private set; }
+
+        #endregion
+
+        #region Ctor
+
+        /// <summary>
+        /// Initializes a new instance of the FileSystemDataExportService class
+        /// </summary>
+        public FileSystemDataExportService(IConfiguration configuration)
+        {
+            _configuration = configuration;
+
+            FileSystemExportOptions = configuration.Get<FileSystemExportOptions>();
+
+            configuration.Bind("FileSystemExportOptions", FileSystemExportOptions);
+
+            Directory.CreateDirectory(FileSystemExportOptions.Path);
+        }
+
+        #endregion
+
+        #region Implementation
+
+        /// <summary>
+        /// Exports the fetched data to the target system
+        /// </summary>
+        /// <param name="depot">The depot data export.</param>
+        public void ExportData(Depot depot)
+        {
+            var filename = String.Format("data_{0:yyyy-MM-dd_HH-mm-ss}.json", DateTime.Now);
+            var path = Path.Combine(FileSystemExportOptions.Path, filename);
+
+            File.WriteAllText(path, JsonConvert.SerializeObject(depot));
+        }
+
+        /// <summary>
+        /// Exports the fetched data from file to the target system
+        /// </summary>
+        /// <param name="file">The JSON file containing the depot data.</param>
+        public void ExportData(string file)
+        {
+            // Skip unnecessary method
+        }
+
+        /// <summary>
+        /// Exports additional data to the target system
+        /// </summary>
+        /// <param name="additionalData">The additional export data.</param>
+        public void ExportAdditionalData(AdditionalData additionalData)
+        {
+            var filename = String.Format("additionalData_{0:yyyy-MM-dd_HH-mm-ss}.json", DateTime.Now);
+            var path = Path.Combine(FileSystemExportOptions.Path, filename);
+
+            File.WriteAllText(path, JsonConvert.SerializeObject(additionalData));
+        }
+
+        #endregion
+    }
+}

+ 42 - 0
GreenTree.Banking.API.comdirect/Services/IDataExportService.cs

@@ -0,0 +1,42 @@
+using GreenTree.Banking.API.comdirect.Data;
+using GreenTree.Banking.API.comdirect.Data.Depot;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Services
+{
+    public interface IDataExportService
+    {
+        #region Properties
+
+        /// <summary>
+        /// Activation status
+        /// </summary>
+        public bool Activated { get; }
+
+        #endregion
+
+        #region Exporting
+
+        /// <summary>
+        /// Exports the fetched data to the target system
+        /// </summary>
+        /// <param name="depot">The depot data export.</param>
+        public void ExportData(Depot depot);
+
+        /// <summary>
+        /// Exports the fetched data from file to the target system
+        /// </summary>
+        /// <param name="file">The JSON file containing the depot data.</param>
+        public void ExportData(string file);
+
+        /// <summary>
+        /// Exports additional data to the target system
+        /// </summary>
+        /// <param name="additionalData">The additional export data.</param>
+        public void ExportAdditionalData(AdditionalData additionalData);
+
+        #endregion
+    }
+}

+ 400 - 0
GreenTree.Banking.API.comdirect/Services/MySqlDataExportService.cs

@@ -0,0 +1,400 @@
+using GreenTree.Banking.API.comdirect.Configuration;
+using GreenTree.Banking.API.comdirect.Data.Depot;
+using GreenTree.Banking.API.comdirect.Extension;
+using Microsoft.Extensions.Configuration;
+using MySqlConnector;
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace GreenTree.Banking.API.comdirect.Services
+{
+    public class MySqlDataExportService : IDataExportService
+    {
+        #region DI fields
+
+        // The global configuration
+        private readonly IConfiguration _configuration;
+
+        #endregion
+
+        #region Properties
+
+        /// <summary>
+        /// Activation status
+        /// </summary>
+        public bool Activated 
+        { 
+            get
+            {
+                return MySqlExportOptions == null ? false : MySqlExportOptions.Activated;
+            }
+        }
+
+        /// <summary>
+        /// The appropriate options for the data export
+        /// </summary>
+        public MySqlExportOptions MySqlExportOptions { get; private set; }
+
+        #endregion
+
+        #region Ctor
+
+        /// <summary>
+        /// Initializes a new instance of the FileSystemDataExportService class
+        /// </summary>
+        public MySqlDataExportService(IConfiguration configuration)
+        {
+            _configuration = configuration;
+
+            MySqlExportOptions = configuration.Get<MySqlExportOptions>();
+
+            configuration.Bind("MySqlExportOptions", MySqlExportOptions);
+
+            CheckDb();
+        }
+
+        #endregion
+
+        #region Implementation
+
+        /// <summary>
+        /// Exports the fetched data to the target system
+        /// </summary>
+        /// <param name="depot">The depot data export.</param>
+        public void ExportData(Depot depot)
+        {
+            using (var connection = new MySqlConnection(MySqlExportOptions.ConnectionString))
+            {
+                connection.Open();
+
+                CreateDepot(depot, connection);
+                CreateValues(depot, connection);
+                AddPrices(depot, connection);
+            }
+        }
+
+        /// <summary>
+        /// Exports the fetched data from file to the target system
+        /// </summary>
+        /// <param name="file">The JSON file containing the depot data.</param>
+        public void ExportData(string file)
+        {
+            var depot = JsonConvert.DeserializeObject<Depot>(File.ReadAllText(file));
+
+            ExportData(depot);
+        }
+
+        /// <summary>
+        /// Exports additional data to the target system
+        /// </summary>
+        /// <param name="additionalData">The additional export data.</param>
+        public void ExportAdditionalData(AdditionalData additionalData)
+        {
+            using (var connection = new MySqlConnection(MySqlExportOptions.ConnectionString))
+            {
+                connection.Open();
+
+                ComplementAdditionalData(additionalData, connection);
+            }
+        }
+
+        #endregion
+
+        #region DB operations
+
+        /// <summary>
+        /// Checks the Database existence and creates it if necessary
+        /// </summary>
+        private void CheckDb()
+        {
+            using (var connection = new MySqlConnection(MySqlExportOptions.ConnectionString))
+            {
+                connection.Open();
+
+                var checkDbCmd = new MySqlCommand(
+                    "CREATE DATABASE IF NOT EXISTS comdirectAPI;",
+                    connection);
+
+                var nonQueryResult = checkDbCmd.ExecuteNonQuery();
+            }
+        }
+
+        /// <summary>
+        /// Creates the the depot if it don't exist already
+        /// </summary>
+        /// <param name="depot">The depot data.</param>
+        /// <param name="connection">The MySQL connection.</param>
+        private void CreateDepot(Depot depot, MySqlConnection connection)
+        {
+            int nonQueryResult;
+            object queryScalarResult;
+
+            connection.ChangeDatabase("comdirectAPI");
+
+            if (!CheckTableExistence(connection, "depot"))
+            {
+                var createTableCmd = new MySqlCommand(
+                    "CREATE TABLE depot (" +
+                    "   id VARCHAR(50) NOT NULL," +
+                    "   PRIMARY KEY(id)," +
+                    "   UNIQUE INDEX id_UNIQUE (id ASC) VISIBLE); ",
+                    connection);
+
+                queryScalarResult = createTableCmd.ExecuteScalar();
+            }
+
+            var checkDepotCmd = new MySqlCommand(
+                "SELECT * FROM depot WHERE id = @id",
+                connection);
+
+            checkDepotCmd.Parameters.Add(new MySqlParameter("@id", depot.Values[0].DepotId));
+
+            queryScalarResult = checkDepotCmd.ExecuteScalar();
+
+            if (queryScalarResult == null)
+            {
+                var insertDepotCmd = new MySqlCommand(
+                    "INSERT INTO depot (id) VALUES (@id)",
+                    connection);
+
+                insertDepotCmd.Parameters.Add(new MySqlParameter("@id", depot.Values[0].DepotId));
+
+                nonQueryResult = insertDepotCmd.ExecuteNonQuery();
+            }
+        }
+
+        /// <summary>
+        /// Creates the depot values if they don't exist already
+        /// </summary>
+        /// <param name="depot">The depot data.</param>
+        /// <param name="connection">The MySQL connection.</param>
+        private void CreateValues(Depot depot, MySqlConnection connection)
+        {
+            int nonQueryResult;
+            object queryScalarResult;
+
+            connection.ChangeDatabase("comdirectAPI");
+
+            if (!CheckTableExistence(connection, "value"))
+            {
+                var createTableCmd = new MySqlCommand(
+                    "CREATE TABLE comdirectAPI.value (" +
+                    "   depot_id VARCHAR(50) NOT NULL," +
+                    "   position_id VARCHAR(50) NOT NULL," +
+                    "   wkn VARCHAR(10) NOT NULL," +
+                    "   custodyType VARCHAR(10) NULL," +
+                    "   version VARCHAR(10) NULL," +
+                    "   hedgeability VARCHAR(20) NULL," +
+                    " PRIMARY KEY(depot_id, position_id));",
+                    connection);
+
+                queryScalarResult = createTableCmd.ExecuteScalar();
+            }
+
+            foreach (var val in depot.Values)
+            {
+                var checkValueCmd = new MySqlCommand(
+                    "SELECT * FROM value WHERE depot_id = @depot_id AND position_id = @position_id",
+                    connection);
+
+                checkValueCmd.Parameters.Add(new MySqlParameter("@depot_id", val.DepotId));
+                checkValueCmd.Parameters.Add(new MySqlParameter("@position_id", val.PositionId));
+
+                queryScalarResult = checkValueCmd.ExecuteScalar();
+
+                if (queryScalarResult == null)
+                {
+                    var insertValueCmd = new MySqlCommand(
+                        "INSERT INTO value (depot_id,position_id,wkn,custodyType,version,hedgeability) " +
+                        "VALUES (@depot_id,@position_id,@wkn,@custodyType,@version,@hedgeability)",
+                        connection);
+
+                    insertValueCmd.Parameters.Add(new MySqlParameter("@depot_id", val.DepotId));
+                    insertValueCmd.Parameters.Add(new MySqlParameter("@position_id", val.PositionId));
+                    insertValueCmd.Parameters.Add(new MySqlParameter("@wkn", val.Wkn));
+                    insertValueCmd.Parameters.Add(new MySqlParameter("@custodyType", val.CustodyType));
+                    insertValueCmd.Parameters.Add(new MySqlParameter("@version", val.Version));
+                    insertValueCmd.Parameters.Add(new MySqlParameter("@hedgeability", val.Hedgeability));
+
+                    nonQueryResult = insertValueCmd.ExecuteNonQuery();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Adds the value prices
+        /// </summary>
+        /// <param name="depot">The depot data.</param>
+        /// <param name="connection">The MySQL connection.</param>
+        private void AddPrices(Depot depot, MySqlConnection connection)
+        {
+            int nonQueryResult;
+            object queryScalarResult;
+
+            connection.ChangeDatabase("comdirectAPI");
+
+            if (!CheckTableExistence(connection, "price"))
+            {
+                var createTableCmd = new MySqlCommand(
+                    "CREATE TABLE comdirectAPI.price (" +
+                    "   depot_id VARCHAR(50) NOT NULL," +
+                    "   position_id VARCHAR(50) NOT NULL," +
+                    "   priceTime DATETIME NOT NULL," +
+                    "   quantity FLOAT NOT NULL," +
+                    "   availableQuantity FLOAT NOT NULL," +
+                    "   priceCurrent FLOAT NOT NULL," +
+                    "   pricePurchased FLOAT NOT NULL," +
+                    " PRIMARY KEY(depot_id, position_id, time));",
+                connection);
+
+                queryScalarResult = createTableCmd.ExecuteScalar();
+            }
+
+            foreach (var val in depot.Values)
+            {
+                var checkPriceCmd = new MySqlCommand(
+                    "SELECT * FROM price WHERE depot_id = @depot_id AND position_id = @position_id AND priceTime = @priceTime",
+                    connection);
+
+                checkPriceCmd.Parameters.Add(new MySqlParameter("@depot_id", val.DepotId));
+                checkPriceCmd.Parameters.Add(new MySqlParameter("@position_id", val.PositionId));
+                checkPriceCmd.Parameters.Add(new MySqlParameter("@priceTime", DateTime.Parse(val.CurrentPrice.PriceDateTime)));
+
+                queryScalarResult = checkPriceCmd.ExecuteScalar();
+
+                if (queryScalarResult == null)
+                {
+                    var insertPriceCmd = new MySqlCommand(
+                        "INSERT INTO price (depot_id,position_id,priceTime,quantity,availableQuantity,priceCurrent,pricePurchased) " +
+                        "VALUES (@depot_id,@position_id,@priceTime,@quantity,@availableQuantity,@priceCurrent,@pricePurchased)",
+                        connection);
+
+                    insertPriceCmd.Parameters.Add(new MySqlParameter("@depot_id", val.DepotId));
+                    insertPriceCmd.Parameters.Add(new MySqlParameter("@position_id", val.PositionId));
+                    insertPriceCmd.Parameters.Add(new MySqlParameter("@priceTime", 
+                        DateTime.Parse(val.CurrentPrice.PriceDateTime)));
+                    insertPriceCmd.Parameters.Add(new MySqlParameter("@quantity", 
+                        Convert.ToSingle(val.Quantity.Value)));
+                    insertPriceCmd.Parameters.Add(new MySqlParameter("@availableQuantity", 
+                        Convert.ToSingle(val.AvailableQuantity.Value)));
+                    insertPriceCmd.Parameters.Add(new MySqlParameter("@priceCurrent", 
+                        Convert.ToSingle(val.CurrentPrice.PriceValue.Value)));
+                    insertPriceCmd.Parameters.Add(new MySqlParameter("@pricePurchased",
+                        Convert.ToSingle(val.PurchasePrice.Value)));
+
+                    nonQueryResult = insertPriceCmd.ExecuteNonQuery();
+                }
+            }
+        }
+
+        /// <summary>
+        /// Complement the value data by additional descriptive data
+        /// </summary>
+        /// <param name="additionalData">The additional data.</param>
+        /// <param name="connection">The MySQL connection.</param>
+        private void ComplementAdditionalData(AdditionalData additionalData, MySqlConnection connection)
+        {
+            int nonQueryResult;
+            object queryScalarResult;
+
+            connection.ChangeDatabase("comdirectAPI");
+
+            if (!CheckColumnExistence(connection, "value", "instrumentId"))
+            {
+                var alterTableCmd = new MySqlCommand(
+                    "ALTER TABLE value" +
+                    "   ADD COLUMN instrumentId VARCHAR(50) NULL AFTER hedgeability," +
+                    "   ADD COLUMN isin VARCHAR(20) NULL AFTER instrumentId," +
+                    "   ADD COLUMN mnemonic VARCHAR(10) NULL AFTER isin," +
+                    "   ADD COLUMN name VARCHAR(80) NULL AFTER mnemonic," +
+                    "   ADD COLUMN shortName VARCHAR(30) NULL AFTER name," +
+                    "   ADD COLUMN instrumentType VARCHAR(10) NULL AFTER shortName;",
+                connection);
+
+                queryScalarResult = alterTableCmd.ExecuteScalar();
+            }
+
+            foreach (var val in additionalData.Values)
+            {
+                var checkValueCmd = new MySqlCommand(
+                    "SELECT * FROM value WHERE wkn = @wkn",
+                    connection);
+
+                checkValueCmd.Parameters.Add(new MySqlParameter("@wkn", val.Instrument.Wkn));
+
+                var valueResult = checkValueCmd.ExecuteFirst();
+
+                if (valueResult != null)
+                {
+                    var updateValueCmd = new MySqlCommand(
+                        "UPDATE value " +
+                        "SET instrumentId = @instrumentId, isin = @isin, mnemonic = @mnemonic, " +
+                        "    name = @name, shortName = @shortName, instrumentType = @instrumentType " +
+                        "WHERE wkn = @wkn;",
+                        connection);
+
+                    updateValueCmd.Parameters.Add(new MySqlParameter("@wkn", val.Instrument.Wkn));
+                    updateValueCmd.Parameters.Add(new MySqlParameter("@instrumentId", val.Instrument.InstrumentId));
+                    updateValueCmd.Parameters.Add(new MySqlParameter("@isin", val.Instrument.Isin));
+                    updateValueCmd.Parameters.Add(new MySqlParameter("@mnemonic", val.Instrument.Mnemonic));
+                    updateValueCmd.Parameters.Add(new MySqlParameter("@name", val.Instrument.Name));
+                    updateValueCmd.Parameters.Add(new MySqlParameter("@shortName", val.Instrument.ShortName));
+                    updateValueCmd.Parameters.Add(new MySqlParameter("@instrumentType", val.Instrument.StaticData.InstrumentType));
+
+                    nonQueryResult = updateValueCmd.ExecuteNonQuery();
+                }
+            }
+        }
+
+        #endregion
+
+        #region Helper
+
+        /// <summary>
+        /// Checks wether a table already exists in the connection database
+        /// </summary>
+        /// <param name="connection">The MySQL connection.</param>
+        /// <param name="tableName">The table name.</param>
+        /// <returns></returns>
+        private bool CheckTableExistence(MySqlConnection connection, string tableName)
+        {
+            var checkTableCmd = new MySqlCommand(
+                String.Format(
+                    "SELECT EXISTS(" +
+                    "    SELECT * FROM information_schema.tables" +
+                    "    WHERE table_schema = 'comdirectAPI'" +
+                    "    AND table_name = '{0}'" +
+                    "); ", tableName),
+                connection);
+
+            return (Int64)checkTableCmd.ExecuteScalar() != 0;
+        }
+
+        /// <summary>
+        /// Checks wether a column already exists in the connection database
+        /// </summary>
+        /// <param name="connection">The MySQL connection.</param>
+        /// <param name="tableName">The table name.</param>
+        /// <param name="columnName">The column name.</param>
+        /// <returns></returns>
+        private bool CheckColumnExistence(MySqlConnection connection, string tableName, string columnName)
+        {
+            var checkColumnCmd = new MySqlCommand(
+                String.Format(
+                    "SELECT EXISTS(" +
+                    "    SELECT * FROM information_schema.columns" +
+                    "    WHERE table_schema = 'comdirectAPI'" +
+                    "    AND table_name = '{0}'" +
+                    "    AND column_name = '{1}'" +
+                    "); ", tableName, columnName),
+                connection);
+
+            return (Int64)checkColumnCmd.ExecuteScalar() != 0;
+        }
+
+        #endregion
+    }
+}

+ 496 - 0
GreenTree.Banking.API.comdirect/Web/Session.cs

@@ -0,0 +1,496 @@
+using GreenTree.Banking.API.comdirect.Configuration;
+using GreenTree.Banking.API.comdirect.Data;
+using GreenTree.Banking.API.comdirect.Data.Depot;
+using GreenTree.Banking.API.comdirect.Extension;
+using GreenTree.Banking.API.comdirect.Services;
+using Newtonsoft.Json;
+using RestSharp;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading;
+
+namespace GreenTree.Banking.API.comdirect.Web
+{
+    public class Session
+    {
+        #region DI fields
+
+        /// <summary>
+        /// The API authentication options
+        /// </summary>
+        private readonly AuthenticationOptions _authenticationOptions;
+
+        /// <summary>
+        /// The global session options
+        /// </summary>
+        private readonly SessionOptions _sessionOptions;
+
+        /// <summary>
+        /// The global data export service provider
+        /// </summary>
+        private readonly IDataExportService _dataExportService;
+
+        #endregion
+
+        #region Fields
+
+        // The REST client that sends requests to the API (values)
+        private RestClient valueRestClient;
+
+        // The REST client that sends requests to the API (authentication)
+        private RestClient authRestClient;
+
+        // The variables which shall be 
+        private Dictionary<string, string> sessionVariables = new Dictionary<string, string>();
+
+        // The cookies to be used on any request
+        private IList<RestResponseCookie> cookies;
+
+        #endregion
+
+        #region Ctor
+
+        /// <summary>
+        /// Initializes a new instance of the Session class
+        /// </summary>
+        /// <param name="authenticationOptions">API authentication options.</param>
+        /// <param name="sessionOptions">The web API session options.</param>
+        /// <param name="dataExportService">The current data export service provider.</param>
+        public Session(
+            AuthenticationOptions authenticationOptions,
+            SessionOptions sessionOptions,
+            IDataExportService dataExportService)
+        {
+            _authenticationOptions = authenticationOptions;
+            _sessionOptions = sessionOptions;
+            _dataExportService = dataExportService;
+
+            valueRestClient = new RestClient(authenticationOptions.BaseUrl);
+        }
+
+        #endregion
+
+        #region API authentication methods
+
+        /// <summary>
+        /// Authenticate against the API
+        /// </summary>
+        public void Authenticate()
+        {
+            Console.WriteLine("Begin authentication...");
+
+            InitAuthenticationToken();
+
+            Console.WriteLine("Created authentication token...");
+
+            InitSession();
+
+            Console.WriteLine("Created session...");
+
+            InitSessionTAN();
+
+            Console.WriteLine("Create TAN activation request... waiting {0} seconds for validation...", 
+                _sessionOptions.TimeoutForTwoFactorTANAuthentication);
+
+            Thread.Sleep(_sessionOptions.TimeoutForTwoFactorTANAuthentication * 1000);
+
+            InitSeesionTANActivation();
+
+            Console.WriteLine("Finish authentication...");
+
+            InitAuthenticationFinish();
+        }
+
+        /// <summary>
+        /// Initiate authentication token
+        /// </summary>
+        private void InitAuthenticationToken()
+        {
+            authRestClient = new RestClient(_authenticationOptions.OAuthUrl + "/oauth/token");
+
+            var request = new RestRequest(Method.POST)
+            {
+                Timeout = _sessionOptions.RestTimeout
+            };
+
+            request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
+            request.AddHeader("Accept", "application/json");
+            request.AddParameter("client_id", _authenticationOptions.ClientId);
+            request.AddParameter("client_secret", _authenticationOptions.ClientSecret);
+            request.AddParameter("grant_type", "password");
+            request.AddParameter("username", _authenticationOptions.LoginNumber);
+            request.AddParameter("password", _authenticationOptions.LoginPin);
+
+            var response = authRestClient.Execute(request);
+            cookies = response.Cookies;
+
+            if (response.StatusCode == System.Net.HttpStatusCode.OK)
+            {
+                var responseData = JsonConvert.DeserializeObject<OAuth2Response>(response.Content);
+
+                sessionVariables.Add("access_token", responseData.AccessToken.ToString());
+                sessionVariables.Add("refresh_token", responseData.RefreshToken.ToString());
+                sessionVariables.Add("session_id", Guid.NewGuid().ToString());
+                sessionVariables.Add("request_id", GenerateRequestId());
+            }
+            else
+            {
+                if (response.ErrorException != null)
+                    throw response.ErrorException;
+
+                throw new Exception(String.Format("Status: {0} - {1}", response.StatusCode, response.Content));
+            }
+        }
+
+        /// <summary>
+        /// Initiate the session UUID
+        /// </summary>
+        private void InitSession()
+        {
+            valueRestClient = new RestClient(_authenticationOptions.BaseUrl + "/session/clients/user/v1/sessions");
+
+            var request = new RestRequest(Method.GET)
+            {
+                Timeout = _sessionOptions.RestTimeout
+            };
+
+            request.AddHeader("Accept", "application/json");
+            request.AddHeader("Authorization", "Bearer " + sessionVariables["access_token"]);
+            request.AddHeader("x-http-request-info", 
+                "{\"clientRequestId\":{\"sessionId\":\"" + sessionVariables["session_id"] + "\",\"requestId\":\"" + 
+                sessionVariables["request_id"] + "\"}}");
+            request.AddHeader("Content-Type", "application/json");
+            request.AddCookies(cookies);
+
+            var response = valueRestClient.Execute(request);
+            cookies = response.Cookies;
+
+            if (response.StatusCode == System.Net.HttpStatusCode.OK)
+            {
+                var responseData = JsonConvert.DeserializeObject<SessionResponse[]>(response.Content);
+
+                sessionVariables.Add("session_uuid", responseData[0].Identifier);
+            }
+            else
+            {
+                if (response.ErrorException != null)
+                    throw response.ErrorException;
+
+                throw new Exception(String.Format("Status: {0} - {1}", response.StatusCode, response.Content));
+            }
+        }
+
+        /// <summary>
+        /// Initiate the session TAN
+        /// </summary>
+        private void InitSessionTAN()
+        {
+            valueRestClient = new RestClient(
+                _authenticationOptions.BaseUrl + "/session/clients/user/v1/sessions/" + sessionVariables["session_uuid"] + "/validate");
+
+            var request = new RestRequest(Method.POST)
+            {
+                Timeout = _sessionOptions.RestTimeout
+            };
+
+            request.AddHeader("Accept", "application/json");
+            request.AddHeader("Authorization", "Bearer " + sessionVariables["access_token"]);
+            request.AddHeader("x-http-request-info",
+                "{\"clientRequestId\":{\"sessionId\":\"" + sessionVariables["session_id"] + "\",\"requestId\":\"" + 
+                sessionVariables["request_id"] + "\"}}");
+            request.AddHeader("Content-Type", "application/json");
+            request.AddCookies(cookies);
+            request.AddParameter("application/json", 
+                "{\"identifier\":\"" + sessionVariables["session_uuid"] + "\",\"sessionTanActive\": true,\"activated2FA\": true\r\n}", 
+                ParameterType.RequestBody);
+
+            var response = valueRestClient.Execute(request);
+            cookies = response.Cookies;
+
+            if (response.StatusCode == System.Net.HttpStatusCode.Created)
+            {
+                foreach (var responseHeader in response.Headers)
+                {
+                    if (responseHeader.Name == "x-once-authentication-info")
+                    {
+                        var responseData = JsonConvert.DeserializeObject<SessionTANResponse>(responseHeader.Value.ToString());
+
+                        sessionVariables.Add("challange_id", responseData.Id.ToString());
+                    }
+                }
+            }
+            else
+            {
+                if (response.ErrorException != null)
+                    throw response.ErrorException;
+
+                throw new Exception(String.Format("Status: {0} - {1}", response.StatusCode, response.Content));
+            }
+        }
+
+        /// <summary>
+        /// Initiate the TAN activation
+        /// </summary>
+        private void InitSeesionTANActivation()
+        {
+            valueRestClient = new RestClient(
+                _authenticationOptions.BaseUrl + "/session/clients/user/v1/sessions/" + sessionVariables["session_uuid"]);
+
+            var request = new RestRequest(Method.PATCH)
+            {
+                Timeout = _sessionOptions.RestTimeout
+            };
+
+            request.AddHeader("Accept", "application/json");
+            request.AddHeader("Authorization", "Bearer " + sessionVariables["access_token"]);
+            request.AddHeader("x-http-request-info",
+                "{\"clientRequestId\":{\"sessionId\":\"" + sessionVariables["session_id"] + "\",\"requestId\":\"" + 
+                sessionVariables["request_id"] + "\"}}");
+            request.AddHeader("Content-Type", "application/json");
+            request.AddHeader("x-once-authentication-info", "{\"id\":\"" + sessionVariables["challange_id"] + "\"}");
+            request.AddCookies(cookies);
+            request.AddParameter("application/json",
+                "{\"identifier\":\"" + sessionVariables["session_uuid"] + "\",\"sessionTanActive\": true,\"activated2FA\": true\r\n}",
+                ParameterType.RequestBody);
+
+            var response = valueRestClient.Execute(request);
+            cookies = response.Cookies;
+
+            if (response.StatusCode == System.Net.HttpStatusCode.OK)
+            {
+
+            }
+            else
+            {
+                if (response.ErrorException != null)
+                    throw response.ErrorException;
+
+                throw new Exception(String.Format("Status: {0} - {1}", response.StatusCode, response.Content));
+            }
+        }
+
+        /// <summary>
+        /// Initiate the finalisation of the authentication
+        /// </summary>
+        private void InitAuthenticationFinish()
+        {
+            authRestClient = new RestClient(_authenticationOptions.OAuthUrl + "/oauth/token");
+
+            var request = new RestRequest(Method.POST)
+            {
+                Timeout = _sessionOptions.RestTimeout
+            };
+
+            request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
+            request.AddHeader("Accept", "application/json");
+            request.AddParameter("client_id", _authenticationOptions.ClientId);
+            request.AddParameter("client_secret", _authenticationOptions.ClientSecret);
+            request.AddParameter("grant_type", "cd_secondary");
+            request.AddParameter("token", sessionVariables["access_token"]);
+
+            var response = authRestClient.Execute(request);
+            cookies = response.Cookies;
+
+            if (response.StatusCode == System.Net.HttpStatusCode.OK)
+            {
+                var responseData = JsonConvert.DeserializeObject<OAuth2Response>(response.Content);
+
+                if (!sessionVariables.ContainsKey("access_token"))
+                    throw new Exception(
+                        "The access token must be initiated first. Use the method 'InitAuthenticationToken' to begin the session authentication.");
+
+                if (!sessionVariables.ContainsKey("refresh_token"))
+                    throw new Exception(
+                        "The refresh token must be initiated first. Use the method 'InitAuthenticationToken' to begin the session authentication.");
+
+                sessionVariables["access_token"] = responseData.AccessToken.ToString();
+                sessionVariables["refresh_token"] = responseData.RefreshToken.ToString();
+            }
+            else
+            {
+                if (response.ErrorException != null)
+                    throw response.ErrorException;
+
+                throw new Exception(String.Format("Status: {0} - {1}", response.StatusCode, response.Content));
+            }
+        }
+
+        /// <summary>
+        /// Refresh current authenticated session
+        /// </summary>
+        public void RefreshAuthentication()
+        {
+            authRestClient = new RestClient(_authenticationOptions.OAuthUrl + "/oauth/token");
+
+            var request = new RestRequest(Method.POST)
+            {
+                Timeout = _sessionOptions.RestTimeout
+            };
+
+            request.AddHeader("Content-Type", "application/x-www-form-urlencoded");
+            request.AddHeader("Accept", "application/json");
+            request.AddParameter("client_id", _authenticationOptions.ClientId);
+            request.AddParameter("client_secret", _authenticationOptions.ClientSecret);
+            request.AddParameter("grant_type", "refresh_token");
+            request.AddParameter("refresh_token", sessionVariables["refresh_token"]);
+
+            var response = authRestClient.Execute(request);
+            cookies = response.Cookies;
+
+            if (response.StatusCode == System.Net.HttpStatusCode.OK)
+            {
+                var responseData = JsonConvert.DeserializeObject<OAuth2Response>(response.Content);
+
+                if (!sessionVariables.ContainsKey("access_token"))
+                    throw new Exception(
+                        "The access token must be initiated first. Use the method 'InitAuthenticationToken' to begin the session authentication.");
+
+                if (!sessionVariables.ContainsKey("refresh_token"))
+                    throw new Exception(
+                        "The refresh token must be initiated first. Use the method 'InitAuthenticationToken' to begin the session authentication.");
+
+                sessionVariables["access_token"] = responseData.AccessToken.ToString();
+                sessionVariables["refresh_token"] = responseData.RefreshToken.ToString();
+            }
+            else
+            {
+                if (response.ErrorException != null)
+                    throw response.ErrorException;
+
+                throw new Exception(String.Format("Status: {0} - {1}", response.StatusCode, response.Content));
+            }
+        }
+
+        #endregion
+
+        #region API data methods
+
+        /// <summary>
+        /// Get the default depot
+        /// </summary>
+        public void GetDefaultDepot()
+        {
+            valueRestClient = new RestClient(_authenticationOptions.BaseUrl + "/brokerage/clients/user/v3/depots");
+
+            var request = new RestRequest(Method.GET)
+            {
+                Timeout = _sessionOptions.RestTimeout
+            };
+
+            request.AddHeader("Accept", "application/json");
+            request.AddHeader("Authorization", "Bearer " + sessionVariables["access_token"]);
+            request.AddHeader("x-http-request-info",
+                "{\"clientRequestId\":{\"sessionId\":\"" + sessionVariables["session_id"] + "\",\"requestId\":\"" + 
+                sessionVariables["request_id"] + "\"}}");
+            request.AddHeader("Content-Type", "application/json");
+
+            var response = valueRestClient.Execute(request);
+            cookies = response.Cookies;
+
+            if (response.StatusCode == System.Net.HttpStatusCode.OK)
+            {
+                var responseData = JsonConvert.DeserializeObject<Depot>(response.Content);
+
+                sessionVariables.Add("depotUUID", responseData.Values[0].DepotId);
+            }
+            else
+            {
+                if (response.ErrorException != null)
+                    throw response.ErrorException;
+
+                throw new Exception(String.Format("Status: {0} - {1}", response.StatusCode, response.Content));
+            }
+        }
+
+        /// <summary>
+        /// Get the current portfolio data of the default depot
+        /// </summary>
+        public Depot GetDepotPortfolioData()
+        {
+            valueRestClient = new RestClient(_authenticationOptions.BaseUrl + "/brokerage/v3/depots/" + sessionVariables["depotUUID"] + "/positions");
+
+            var request = new RestRequest(Method.GET)
+            {
+                Timeout = _sessionOptions.RestTimeout
+            };
+
+            request.AddHeader("Accept", "application/json");
+            request.AddHeader("Authorization", "Bearer " + sessionVariables["access_token"]);
+            request.AddHeader("x-http-request-info",
+                "{\"clientRequestId\":{\"sessionId\":\"" + sessionVariables["session_id"] + 
+                "\",\"requestId\":\"" + sessionVariables["request_id"] + "\"}}");
+            request.AddHeader("Content-Type", "application/json");
+
+            var response = valueRestClient.Execute(request);
+            cookies = response.Cookies;
+
+            if (response.StatusCode == System.Net.HttpStatusCode.OK)
+            {
+                var responseData = JsonConvert.DeserializeObject<Depot>(response.Content);
+
+                return responseData;
+            }
+            else
+            {
+                if (response.ErrorException != null)
+                    throw response.ErrorException;
+
+                throw new Exception(String.Format("Status: {0} - {1}", response.StatusCode, response.Content));
+            }
+        }
+
+        /// <summary>
+        /// Get the current additional portfolio data
+        /// </summary>
+        public AdditionalData GetDepotRevenueData()
+        {
+            valueRestClient = new RestClient(_authenticationOptions.BaseUrl + "/brokerage/v3/depots/" + sessionVariables["depotUUID"] + "/transactions");
+
+            var request = new RestRequest(Method.GET)
+            {
+                Timeout = _sessionOptions.RestTimeout
+            };
+
+            request.AddHeader("Accept", "application/json");
+            request.AddHeader("Authorization", "Bearer " + sessionVariables["access_token"]);
+            request.AddHeader("x-http-request-info",
+                "{\"clientRequestId\":{\"sessionId\":\"" + sessionVariables["session_id"] +
+                "\",\"requestId\":\"" + sessionVariables["request_id"] + "\"}}");
+            request.AddHeader("Content-Type", "application/json");
+
+            var response = valueRestClient.Execute(request);
+            cookies = response.Cookies;
+
+            if (response.StatusCode == System.Net.HttpStatusCode.OK)
+            {
+                var responseData = JsonConvert.DeserializeObject<AdditionalData>(response.Content);
+
+                return responseData;
+            }
+            else
+            {
+                if (response.ErrorException != null)
+                    throw response.ErrorException;
+
+                throw new Exception(String.Format("Status: {0} - {1}", response.StatusCode, response.Content));
+            }
+        }
+
+        #endregion
+
+        #region Helper
+
+        /// <summary>
+        /// Generates a request id from the current timestamp
+        /// </summary>
+        private string GenerateRequestId()
+        {
+            var ticks = DateTime.Now.Ticks.ToString();
+
+            return ticks.Substring(ticks.Length - 10, 9);
+        }
+
+        #endregion
+    }
+}

+ 24 - 0
GreenTree.Banking.API.comdirect/appsettings.json

@@ -0,0 +1,24 @@
+{
+    "AuthenticationOptions": {
+        "BaseUrl": "https://api.comdirect.de/api",
+        "OAuthUrl": "https://api.comdirect.de",
+        "ClientId": "User_AEDA2AF35AFF49D7A8FB81A6C5F3B15D",
+        "ClientSecret": "157DBCC53A974A808C1F5B7855B19AFD",
+        "LoginNumber": 87683256,
+        "LoginPin": 145958
+    },
+    "FileSystemExportOptions": {
+        "Activated": true,
+        "Path": "data"
+    },
+    "MySqlExportOptions": {
+        "Activated": true,
+        "ConnectionString": "Server=lynx-solutions.org;Port=13307;User Id=root;Password=0A5rPTYC;"
+    },
+    "SessionOptions": {
+        "TimeoutForTwoFactorTANAuthentication": 60, //Seconds
+        "RestTimeout": 60000, //Miliseconds
+        "FetchDataInterval": 15, //Minutes
+        "RefreshSessionInterval": 5 //Minutes
+    }
+}

+ 25 - 0
GreenTree.Banking.sln

@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30204.135
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GreenTree.Banking.API.comdirect", "GreenTree.Banking.API.comdirect\GreenTree.Banking.API.comdirect.csproj", "{F71B20B8-2CB9-4D7C-8960-6F116F84BDB0}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Any CPU = Debug|Any CPU
+		Release|Any CPU = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{F71B20B8-2CB9-4D7C-8960-6F116F84BDB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{F71B20B8-2CB9-4D7C-8960-6F116F84BDB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{F71B20B8-2CB9-4D7C-8960-6F116F84BDB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{F71B20B8-2CB9-4D7C-8960-6F116F84BDB0}.Release|Any CPU.Build.0 = Release|Any CPU
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {3D8535BD-2E4A-4554-9CA8-B91EA361B098}
+	EndGlobalSection
+EndGlobal