feat(fundamentals): dynamic settings, IFinlyticLogger, live log streaming, and EF migration
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
|
using FinlyticCore.Database;
|
||||||
using FinlyticCore.Entities.Settings;
|
using FinlyticCore.Entities.Settings;
|
||||||
using FinlyticFundamentals.Entities;
|
using FinlyticFundamentals.Entities;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Design;
|
||||||
|
|
||||||
namespace FinlyticFundamentals.Database;
|
namespace FinlyticFundamentals.Database;
|
||||||
|
|
||||||
public class FundamentalsDbContext : DbContext
|
public class FundamentalsDbContext : DbContext, ISettingsDbContext
|
||||||
{
|
{
|
||||||
public FundamentalsDbContext(DbContextOptions<FundamentalsDbContext> options) : base(options)
|
public FundamentalsDbContext(DbContextOptions<FundamentalsDbContext> options) : base(options)
|
||||||
{
|
{
|
||||||
@@ -23,7 +25,7 @@ public class FundamentalsDbContext : DbContext
|
|||||||
modelBuilder.Entity<SettingEntity>(entity =>
|
modelBuilder.Entity<SettingEntity>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasKey(e => e.Id);
|
entity.HasKey(e => e.Id);
|
||||||
entity.HasIndex(e => e.Key);
|
entity.HasIndex(e => e.Key).IsUnique();
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity<AssetDataEntity>(entity =>
|
modelBuilder.Entity<AssetDataEntity>(entity =>
|
||||||
@@ -93,3 +95,13 @@ public class FundamentalsDbContext : DbContext
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class FundamentalsDbContextFactory : IDesignTimeDbContextFactory<FundamentalsDbContext>
|
||||||
|
{
|
||||||
|
public FundamentalsDbContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
var optionsBuilder = new DbContextOptionsBuilder<FundamentalsDbContext>();
|
||||||
|
optionsBuilder.UseNpgsql("Host=localhost;Database=fundamentals;Username=postgres;Password=postgres");
|
||||||
|
return new FundamentalsDbContext(optionsBuilder.Options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+430
@@ -0,0 +1,430 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using FinlyticFundamentals.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(FundamentalsDbContext))]
|
||||||
|
[Migration("20260815183935_AddDynamicSettings")]
|
||||||
|
partial class AddDynamicSettings
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.9")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticCore.Entities.Settings.SettingEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(150)
|
||||||
|
.HasColumnType("character varying(150)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("ServiceIdentifier")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("ValueJson")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("DynamicSettings");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetDataEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Isin");
|
||||||
|
|
||||||
|
b.ToTable("AssetData");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetEventEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AssetDataIsin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Date")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssetDataIsin");
|
||||||
|
|
||||||
|
b.ToTable("AssetEvents");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.FundamentalDataEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Isin")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("AssetDataIsin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ConsensusRating")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<decimal?>("CurrentRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("DebtToEquity")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("DilutedEps")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("Ebitda")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EnterpriseValue")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("EvToEbitda")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FiftyTwoWeekHigh")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FiftyTwoWeekLow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ForwardDividendYield")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ForwardPe")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("FreeCashFlow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("GrossProfit")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<DateTime>("LastUpdatedUtc")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<decimal?>("MarketCap")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("NetIncome")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("OperatingCashFlow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("OperatingIncome")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PayoutRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PegRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PercentHeldByInsiders")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PercentHeldByInstitutions")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceTargetHigh")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceTargetLow")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceTargetMean")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceToBook")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("PriceToSales")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ReturnOnAssets")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ReturnOnEquity")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("RevenueGrowthYoY")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ShortPercentOfFloat")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("ShortRatio")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalCash")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalDebt")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TotalRevenue")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<decimal?>("TrailingPe")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.HasKey("Isin");
|
||||||
|
|
||||||
|
b.HasIndex("AssetDataIsin");
|
||||||
|
|
||||||
|
b.ToTable("FundamentalData");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.KeyExecutiveEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("AssetDataIsin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Payment")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("SortOrder")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(0);
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AssetDataIsin", "SortOrder");
|
||||||
|
|
||||||
|
b.ToTable("KeyExecutives");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetDataEntity", b =>
|
||||||
|
{
|
||||||
|
b.OwnsOne("FinlyticFundamentals.Entities.TickerEntity", "PrimaryTicker", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<string>("AssetDataEntityIsin")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b1.Property<string>("Exchange")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("PrimaryTickerExchange");
|
||||||
|
|
||||||
|
b1.Property<string>("Ticker")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("PrimaryTicker");
|
||||||
|
|
||||||
|
b1.HasKey("AssetDataEntityIsin");
|
||||||
|
|
||||||
|
b1.ToTable("AssetData");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("AssetDataEntityIsin");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.OwnsMany("FinlyticFundamentals.Entities.TickerEntity", "AvailableTickers", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("AssetDataIsin")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b1.Property<string>("Exchange")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("Exchange");
|
||||||
|
|
||||||
|
b1.Property<string>("Ticker")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("Ticker");
|
||||||
|
|
||||||
|
b1.HasKey("Id");
|
||||||
|
|
||||||
|
b1.HasIndex("AssetDataIsin");
|
||||||
|
|
||||||
|
b1.HasIndex("Ticker");
|
||||||
|
|
||||||
|
b1.ToTable("Tickers", (string)null);
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("AssetDataIsin");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("AvailableTickers");
|
||||||
|
|
||||||
|
b.Navigation("PrimaryTicker")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetEventEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticFundamentals.Entities.AssetDataEntity", "AssetData")
|
||||||
|
.WithMany("AssetEvents")
|
||||||
|
.HasForeignKey("AssetDataIsin")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.OwnsOne("FinlyticFundamentals.Entities.TickerEntity", "Ticker", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<Guid>("AssetEventEntityId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b1.Property<string>("Exchange")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("TickerExchange");
|
||||||
|
|
||||||
|
b1.Property<string>("Ticker")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("Ticker");
|
||||||
|
|
||||||
|
b1.HasKey("AssetEventEntityId");
|
||||||
|
|
||||||
|
b1.ToTable("AssetEvents");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("AssetEventEntityId");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("AssetData");
|
||||||
|
|
||||||
|
b.Navigation("Ticker")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.FundamentalDataEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticFundamentals.Entities.AssetDataEntity", "AssetData")
|
||||||
|
.WithMany("FundamentalData")
|
||||||
|
.HasForeignKey("AssetDataIsin")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.OwnsOne("FinlyticFundamentals.Entities.TickerEntity", "Ticker", b1 =>
|
||||||
|
{
|
||||||
|
b1.Property<string>("FundamentalDataEntityIsin")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b1.Property<string>("Exchange")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("TickerExchange");
|
||||||
|
|
||||||
|
b1.Property<string>("Ticker")
|
||||||
|
.IsRequired()
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasDefaultValue("")
|
||||||
|
.HasColumnName("Ticker");
|
||||||
|
|
||||||
|
b1.HasKey("FundamentalDataEntityIsin");
|
||||||
|
|
||||||
|
b1.ToTable("FundamentalData");
|
||||||
|
|
||||||
|
b1.WithOwner()
|
||||||
|
.HasForeignKey("FundamentalDataEntityIsin");
|
||||||
|
});
|
||||||
|
|
||||||
|
b.Navigation("AssetData");
|
||||||
|
|
||||||
|
b.Navigation("Ticker")
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.KeyExecutiveEntity", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("FinlyticFundamentals.Entities.AssetDataEntity", "AssetData")
|
||||||
|
.WithMany("KeyExecutives")
|
||||||
|
.HasForeignKey("AssetDataIsin")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("AssetData");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("FinlyticFundamentals.Entities.AssetDataEntity", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AssetEvents");
|
||||||
|
|
||||||
|
b.Navigation("FundamentalData");
|
||||||
|
|
||||||
|
b.Navigation("KeyExecutives");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace FinlyticFundamentals.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddDynamicSettings : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_DynamicSettings_Key",
|
||||||
|
table: "DynamicSettings");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DynamicSettings_Key",
|
||||||
|
table: "DynamicSettings",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_DynamicSettings_Key",
|
||||||
|
table: "DynamicSettings");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_DynamicSettings_Key",
|
||||||
|
table: "DynamicSettings",
|
||||||
|
column: "Key");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,8 @@ namespace FinlyticFundamentals.Migrations
|
|||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
b.HasIndex("Key");
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("DynamicSettings");
|
b.ToTable("DynamicSettings");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1,26 @@
|
|||||||
using FinlyticCore.Clients;
|
using FinlyticCore.Clients;
|
||||||
|
using FinlyticCore.Database;
|
||||||
using FinlyticCore.Services;
|
using FinlyticCore.Services;
|
||||||
using FinlyticCore.Services.PlaywrightScrapper;
|
using FinlyticCore.Services.PlaywrightScrapper;
|
||||||
using FinlyticCore.Services.TradeRepublic;
|
using FinlyticCore.Services.TradeRepublic;
|
||||||
|
using FinlyticCore.Services.Yahoo;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using FinlyticFundamentals.Database;
|
using FinlyticFundamentals.Database;
|
||||||
using FinlyticFundamentals.Services;
|
using FinlyticFundamentals.Services;
|
||||||
using FinlyticFundamentals.Util;
|
using FinlyticFundamentals.Util;
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
// Register DB Context
|
// Register DB Context & ISettingsDbContext
|
||||||
builder.Services.AddDbContext<FundamentalsDbContext>(options =>
|
builder.Services.AddDbContext<FundamentalsDbContext>(options =>
|
||||||
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))
|
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))
|
||||||
.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)));
|
.ConfigureWarnings(w => w.Ignore(RelationalEventId.PendingModelChangesWarning)));
|
||||||
|
builder.Services.AddScoped<ISettingsDbContext>(sp => sp.GetRequiredService<FundamentalsDbContext>());
|
||||||
|
|
||||||
// Register HTTP Clients
|
// Register HTTP Clients
|
||||||
builder.Services.AddHttpClient<IYahooFinanceScraper, YahooFinanceScraper>()
|
builder.Services.AddHttpClient<IYahooFinanceScraper, YahooFinanceScraper>()
|
||||||
@@ -25,22 +31,23 @@ builder.Services.AddHttpClient<IYahooFinanceScraper, YahooFinanceScraper>()
|
|||||||
AllowAutoRedirect = true
|
AllowAutoRedirect = true
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Services.AddSingleton<IPlaywrightBrowserFactory, PlaywrightBrowserFactory>();
|
builder.Services.AddScoped<IPlaywrightBrowserFactory, PlaywrightBrowserFactory>();
|
||||||
builder.Services.AddSingleton<IPlaywrightExecutionService, PlaywrightExecutionService>();
|
builder.Services.AddScoped<IPlaywrightExecutionService, PlaywrightExecutionService>();
|
||||||
builder.Services.AddTransient<IYahooFinanceHtmlClient, YahooFinanceHtmlClient<FundamentalsDbService, FundamentalsDbContext>>();
|
builder.Services.AddTransient<IYahooFinanceHtmlClient, YahooFinanceHtmlClient>();
|
||||||
|
|
||||||
// Register Application Services
|
// Register Application Services
|
||||||
builder.Services.AddSingleton<TradeRepublicClient>();
|
builder.Services.AddScoped<TradeRepublicClient>();
|
||||||
builder.Services.AddSingleton<ITradeRepublicService, TradeRepublicService>();
|
builder.Services.AddScoped<ITradeRepublicService, TradeRepublicService>();
|
||||||
builder.Services.AddSingleton<FinlyticCore.Services.Yahoo.YahooFinanceClient>();
|
builder.Services.AddSingleton<YahooFinanceClient>();
|
||||||
builder.Services.AddTransient<IFundamentalsDbService, FundamentalsDbService>();
|
builder.Services.AddScoped<IFundamentalsDbService, FundamentalsDbService>();
|
||||||
builder.Services.AddScoped<IYahooFinanceScraper, YahooFinanceScraper>();
|
builder.Services.AddScoped<IYahooFinanceScraper, YahooFinanceScraper>();
|
||||||
|
|
||||||
builder.Services.AddScoped(typeof(ISettingsService<>), typeof(SettingsService<>));
|
builder.Services.AddSingleton<ISettingsService, SettingsService>();
|
||||||
builder.Services.AddScoped(typeof(IFinlyticLogger<,>), typeof(FinlyticLogger<,>));
|
builder.Services.AddSingleton(typeof(IFinlyticLogger<>), typeof(FinlyticLogger<>));
|
||||||
|
|
||||||
// Register MQTT Client (as a Hosted Service)
|
// Register MQTT Client (as a Hosted Service)
|
||||||
builder.Services.AddHostedService<FundamentalsMqttClient>();
|
builder.Services.AddSingleton<FundamentalsMqttClient>();
|
||||||
|
builder.Services.AddHostedService(sp => sp.GetRequiredService<FundamentalsMqttClient>());
|
||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
|
|
||||||
@@ -52,7 +59,6 @@ using (var scope = host.Services.CreateScope())
|
|||||||
var context = scope.ServiceProvider.GetRequiredService<FundamentalsDbContext>();
|
var context = scope.ServiceProvider.GetRequiredService<FundamentalsDbContext>();
|
||||||
await context.Database.MigrateAsync();
|
await context.Database.MigrateAsync();
|
||||||
Console.WriteLine("Database migrations successfully executed for FinlyticFundamentals.");
|
Console.WriteLine("Database migrations successfully executed for FinlyticFundamentals.");
|
||||||
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using FinlyticCore.Dtos.Yahoo;
|
|||||||
using FinlyticCore.Models.Settings;
|
using FinlyticCore.Models.Settings;
|
||||||
using FinlyticCore.Services;
|
using FinlyticCore.Services;
|
||||||
using FinlyticCore.Services.TradeRepublic;
|
using FinlyticCore.Services.TradeRepublic;
|
||||||
|
using FinlyticCore.Services.Yahoo;
|
||||||
using FinlyticFundamentals.Database;
|
using FinlyticFundamentals.Database;
|
||||||
using FinlyticFundamentals.Entities;
|
using FinlyticFundamentals.Entities;
|
||||||
using FinlyticFundamentals.Util;
|
using FinlyticFundamentals.Util;
|
||||||
@@ -39,13 +40,13 @@ public class FundamentalsDbService : IFundamentalsDbService
|
|||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
private readonly IYahooFinanceScraper _scraper;
|
private readonly IYahooFinanceScraper _scraper;
|
||||||
private readonly ITradeRepublicService _tradeRepublicService;
|
private readonly ITradeRepublicService _tradeRepublicService;
|
||||||
private readonly IFinlyticLogger<FundamentalsDbService, FundamentalsDbContext> _finlyticLogger;
|
private readonly IFinlyticLogger<FundamentalsDbService> _finlyticLogger;
|
||||||
|
|
||||||
public FundamentalsDbService(
|
public FundamentalsDbService(
|
||||||
IServiceScopeFactory scopeFactory,
|
IServiceScopeFactory scopeFactory,
|
||||||
IYahooFinanceScraper scraper,
|
IYahooFinanceScraper scraper,
|
||||||
ITradeRepublicService tradeRepublicService,
|
ITradeRepublicService tradeRepublicService,
|
||||||
IFinlyticLogger<FundamentalsDbService, FundamentalsDbContext> finlyticLogger)
|
IFinlyticLogger<FundamentalsDbService> finlyticLogger)
|
||||||
{
|
{
|
||||||
_scopeFactory = scopeFactory;
|
_scopeFactory = scopeFactory;
|
||||||
_scraper = scraper;
|
_scraper = scraper;
|
||||||
@@ -71,7 +72,7 @@ public class FundamentalsDbService : IFundamentalsDbService
|
|||||||
{
|
{
|
||||||
using var scope = _scopeFactory.CreateScope();
|
using var scope = _scopeFactory.CreateScope();
|
||||||
var context = scope.ServiceProvider.GetRequiredService<FundamentalsDbContext>();
|
var context = scope.ServiceProvider.GetRequiredService<FundamentalsDbContext>();
|
||||||
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService<FundamentalsDbContext>>();
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||||
|
|
||||||
// 1. Dynamic Settings lesen
|
// 1. Dynamic Settings lesen
|
||||||
bool allowForceRefresh =
|
bool allowForceRefresh =
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FinlyticCore.Dtos;
|
using FinlyticCore.Dtos;
|
||||||
|
using FinlyticCore.Dtos.Settings;
|
||||||
using FinlyticCore.Models;
|
using FinlyticCore.Models;
|
||||||
using FinlyticCore.Services;
|
using FinlyticCore.Services;
|
||||||
using FinlyticCore.Util;
|
using FinlyticCore.Util;
|
||||||
using FinlyticFundamentals.Database;
|
|
||||||
using FinlyticFundamentals.Services;
|
using FinlyticFundamentals.Services;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -60,7 +61,17 @@ public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
|
|||||||
await SubscribeAsync("services/request/fundamentals_Get/#");
|
await SubscribeAsync("services/request/fundamentals_Get/#");
|
||||||
await SubscribeAsync("services/request/events_GetAll/#");
|
await SubscribeAsync("services/request/events_GetAll/#");
|
||||||
await SubscribeAsync("services/request/events_GetByMonth/#");
|
await SubscribeAsync("services/request/events_GetByMonth/#");
|
||||||
|
await SubscribeAsync("services/request/fundamentals_settings_GetAll/#");
|
||||||
|
await SubscribeAsync("services/request/fundamentals_settings_Update/#");
|
||||||
await SubscribeAsync("services/request/health_Ping/#");
|
await SubscribeAsync("services/request/health_Ping/#");
|
||||||
|
|
||||||
|
FinlyticLogBroadcaster.OnLogPublished = async (logDto) =>
|
||||||
|
{
|
||||||
|
if (IsConnected && string.Equals(logDto.ServiceName, "FinlyticFundamentals", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await PublishAsync("finlytic/logs/FinlyticFundamentals", logDto);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@@ -85,6 +96,14 @@ public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
|
|||||||
{
|
{
|
||||||
await OnEventsGetByMonthAsync(payload, correlationId);
|
await OnEventsGetByMonthAsync(payload, correlationId);
|
||||||
}
|
}
|
||||||
|
else if (topic.StartsWith("services/request/fundamentals_settings_GetAll", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await OnSettingsGetAllAsync(correlationId);
|
||||||
|
}
|
||||||
|
else if (topic.StartsWith("services/request/fundamentals_settings_Update", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await OnSettingsUpdateAsync(payload, correlationId);
|
||||||
|
}
|
||||||
else if (topic.StartsWith("services/request/health_Ping", StringComparison.OrdinalIgnoreCase))
|
else if (topic.StartsWith("services/request/health_Ping", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
await OnHealthPingAsync(topic, correlationId);
|
await OnHealthPingAsync(topic, correlationId);
|
||||||
@@ -93,8 +112,8 @@ public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
|
|||||||
|
|
||||||
private async Task OnFundamentalsGetAsync(string payload, string correlationId)
|
private async Task OnFundamentalsGetAsync(string payload, string correlationId)
|
||||||
{
|
{
|
||||||
using var scope = _scopeFactory.CreateScope();
|
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient, FundamentalsDbContext>>();
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient>>();
|
||||||
var dbService = scope.ServiceProvider.GetRequiredService<IFundamentalsDbService>();
|
var dbService = scope.ServiceProvider.GetRequiredService<IFundamentalsDbService>();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(payload))
|
if (string.IsNullOrWhiteSpace(payload))
|
||||||
@@ -129,8 +148,8 @@ public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
|
|||||||
|
|
||||||
private async Task OnEventsGetAllAsync(string correlationId)
|
private async Task OnEventsGetAllAsync(string correlationId)
|
||||||
{
|
{
|
||||||
using var scope = _scopeFactory.CreateScope();
|
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient, FundamentalsDbContext>>();
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient>>();
|
||||||
var dbService = scope.ServiceProvider.GetRequiredService<IFundamentalsDbService>();
|
var dbService = scope.ServiceProvider.GetRequiredService<IFundamentalsDbService>();
|
||||||
|
|
||||||
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticFundamentals] [MQTT_Client] Processing RPC events_GetAll request [CorrelationId: {CorrelationId}]", correlationId);
|
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticFundamentals] [MQTT_Client] Processing RPC events_GetAll request [CorrelationId: {CorrelationId}]", correlationId);
|
||||||
@@ -152,8 +171,8 @@ public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(payload)) return;
|
if (string.IsNullOrWhiteSpace(payload)) return;
|
||||||
|
|
||||||
using var scope = _scopeFactory.CreateScope();
|
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient, FundamentalsDbContext>>();
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient>>();
|
||||||
var dbService = scope.ServiceProvider.GetRequiredService<IFundamentalsDbService>();
|
var dbService = scope.ServiceProvider.GetRequiredService<IFundamentalsDbService>();
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -175,12 +194,78 @@ public class FundamentalsMqttClient : ManagedMqttClient, IHostedService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OnSettingsGetAllAsync(string correlationId)
|
||||||
|
{
|
||||||
|
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||||
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient>>();
|
||||||
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||||
|
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticFundamentals] [Settings_GetAll] Retrieving all dynamic settings via reflection [CorrelationId: {CorrelationId}]", correlationId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var settings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
||||||
|
var responseTopic = $"services/response/fundamentals_settings_GetAll/{correlationId}";
|
||||||
|
|
||||||
|
await PublishAsync(responseTopic, settings);
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticFundamentals] [Settings_GetAll] Published {Count} settings to '{ResponseTopic}'", settings.Count, responseTopic);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticFundamentals] [Settings_GetAll] Failed to retrieve settings.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OnSettingsUpdateAsync(string payload, string correlationId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(payload)) return;
|
||||||
|
|
||||||
|
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||||
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient>>();
|
||||||
|
var settingsService = scope.ServiceProvider.GetRequiredService<ISettingsService>();
|
||||||
|
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticFundamentals] [Settings_Update] Processing settings update RPC [CorrelationId: {CorrelationId}]", correlationId);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Dictionary<string, object?>? updates = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
updates = JsonSerializer.Deserialize<Dictionary<string, object?>>(payload);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
var list = JsonSerializer.Deserialize<List<DynamicSettingDto>>(payload);
|
||||||
|
if (list != null)
|
||||||
|
{
|
||||||
|
updates = new Dictionary<string, object?>();
|
||||||
|
foreach (var item in list)
|
||||||
|
{
|
||||||
|
updates[item.Key] = item.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updates != null && updates.Count > 0)
|
||||||
|
{
|
||||||
|
await settingsService.UpdateSettingsAsync(updates);
|
||||||
|
await finlyticLogger.LogInfoAsync(SettingKeys.MqttChannel, "[FinlyticFundamentals] [Settings_Update] Successfully updated {Count} settings in database and cache.", updates.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentSettings = await settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(SettingKeys) });
|
||||||
|
var responseTopic = $"services/response/fundamentals_settings_Update/{correlationId}";
|
||||||
|
await PublishAsync(responseTopic, currentSettings);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
await finlyticLogger.LogErrorAsync(SettingKeys.MqttChannel, ex, "[FinlyticFundamentals] [Settings_Update] Failed to update settings.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task OnHealthPingAsync(string topic, string correlationId)
|
private async Task OnHealthPingAsync(string topic, string correlationId)
|
||||||
{
|
{
|
||||||
if (topic.Contains("FinlyticFundamentals", StringComparison.OrdinalIgnoreCase) || !topic.Contains("/", StringComparison.OrdinalIgnoreCase))
|
if (topic.Contains("FinlyticFundamentals", StringComparison.OrdinalIgnoreCase) || !topic.Contains("/", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
using var scope = _scopeFactory.CreateScope();
|
await using var scope = _scopeFactory.CreateAsyncScope();
|
||||||
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient, FundamentalsDbContext>>();
|
var finlyticLogger = scope.ServiceProvider.GetRequiredService<IFinlyticLogger<FundamentalsMqttClient>>();
|
||||||
|
|
||||||
var respTopic = $"services/response/health_Ping/{correlationId}";
|
var respTopic = $"services/response/health_Ping/{correlationId}";
|
||||||
await PublishAsync(respTopic, new ServiceHealthResponse("FinlyticFundamentals", "Online", DateTime.UtcNow, "Connected"));
|
await PublishAsync(respTopic, new ServiceHealthResponse("FinlyticFundamentals", "Online", DateTime.UtcNow, "Connected"));
|
||||||
|
|||||||
Reference in New Issue
Block a user