11using GhostfolioSidekick . Database ;
22using GhostfolioSidekick . Database . Repository ;
33using GhostfolioSidekick . Model . Market ;
4+ using GhostfolioSidekick . Model . Symbols ;
45using GhostfolioSidekick . PortfolioViewer . WASM . Data . Models ;
56using Microsoft . EntityFrameworkCore ;
67
78namespace GhostfolioSidekick . PortfolioViewer . WASM . Data . Services
89{
10+ /// <summary>
11+ /// Service for retrieving upcoming dividends for portfolio holdings.
12+ /// </summary>
913 public class UpcomingDividendsService (
1014 IDbContextFactory < DatabaseContext > dbContextFactory ,
1115 ICurrencyExchange currencyExchange ,
@@ -14,11 +18,7 @@ public class UpcomingDividendsService(
1418 public async Task < List < UpcomingDividendModel > > GetUpcomingDividendsAsync ( )
1519 {
1620 await using var databaseContext = await dbContextFactory . CreateDbContextAsync ( ) ;
17-
18- // Get the primary currency to convert all amounts to
1921 var primaryCurrency = await serverConfigurationService . GetPrimaryCurrencyAsync ( ) ;
20-
21- // Get the latest date for calculated snapshots
2222 var lastKnownDate = await databaseContext . CalculatedSnapshots
2323 . MaxAsync ( x => ( DateOnly ? ) x . Date ) ;
2424
@@ -27,95 +27,105 @@ public async Task<List<UpcomingDividendModel>> GetUpcomingDividendsAsync()
2727 return [ ] ;
2828 }
2929
30- // Fetch all holdings and their symbol profiles
31- var holdingsWithProfiles = await databaseContext . Holdings
32- . Include ( h => h . SymbolProfiles )
33- . ToListAsync ( ) ;
34-
35- // Fetch all calculated snapshots for the latest date
36- var snapshots = await databaseContext . CalculatedSnapshots
37- . Where ( s => s . Date == lastKnownDate )
38- . ToListAsync ( ) ;
39-
40- // Build holdings dictionary: symbol -> total quantity
41- var holdingsDict = new Dictionary < string , decimal > ( ) ;
42- foreach ( var holding in holdingsWithProfiles )
43- {
44- var quantity = snapshots
45- . Where ( s => s . HoldingId == holding . Id )
46- . Sum ( s => s . Quantity ) ;
47-
48- var sp = holding . SymbolProfiles . FirstOrDefault ( ) ;
49- var symbol = sp ? . Symbol ;
50- if ( ! string . IsNullOrEmpty ( symbol ) && quantity > 0 )
51- {
52- if ( holdingsDict . ContainsKey ( symbol ) )
53- holdingsDict [ symbol ] += quantity ;
54- else
55- holdingsDict [ symbol ] = quantity ;
56- }
57- }
58-
59- // Get upcoming dividends, join with SymbolProfiles using explicit properties
60- var today = DateOnly . FromDateTime ( DateTime . Today ) ;
61- var dividends = await databaseContext . Dividends
62- . Where ( dividend => dividend . PaymentDate >= today )
63- . Join ( databaseContext . SymbolProfiles ,
64- dividend => new { Symbol = dividend . SymbolProfileSymbol , DataSource = dividend . SymbolProfileDataSource } ,
65- symbolProfile => new { Symbol = ( string ? ) symbolProfile . Symbol , DataSource = ( string ? ) symbolProfile . DataSource } ,
66- ( dividend , symbolProfile ) => new { Dividend = dividend , SymbolProfile = symbolProfile } )
67- . Where ( x => x . Dividend . Amount . Amount > 0 )
68- . ToListAsync ( ) ;
30+ var holdingsDict = await GetHoldingsDictionaryAsync ( databaseContext , lastKnownDate . Value ) ;
31+ var dividendsWithProfiles = await GetUpcomingDividendsWithProfilesAsync ( databaseContext ) ;
6932
7033 var result = new List < UpcomingDividendModel > ( ) ;
71- foreach ( var item in dividends )
34+ foreach ( var item in dividendsWithProfiles )
7235 {
7336 var symbol = item . SymbolProfile . Symbol ?? string . Empty ;
7437 var companyName = item . SymbolProfile . Name ?? string . Empty ;
75- holdingsDict . TryGetValue ( symbol , out var quantity ) ;
76-
77- if ( quantity <= 0 )
38+ if ( ! holdingsDict . TryGetValue ( symbol , out var quantity ) || quantity <= 0 )
7839 {
7940 continue ;
8041 }
8142
82- // Native currency values (original dividend currency)
8343 var dividendPerShare = item . Dividend . Amount . Amount ;
8444 var expectedAmount = dividendPerShare * quantity ;
8545 var nativeCurrency = item . Dividend . Amount . Currency . Symbol ;
8646
87- // Convert dividend per share to primary currency
88- var dividendPerShareConverted = await currencyExchange . ConvertMoney (
89- item . Dividend . Amount ,
90- primaryCurrency ,
91- item . Dividend . ExDividendDate ) ;
92-
93- var dividendPerSharePrimaryCurrency = dividendPerShareConverted . Amount ;
94- var expectedAmountPrimaryCurrency = dividendPerSharePrimaryCurrency * quantity ;
47+ decimal ? dividendPerSharePrimaryCurrency = null ;
48+ decimal ? expectedAmountPrimaryCurrency = null ;
49+ string primaryCurrencyLabel = primaryCurrency . Symbol ;
50+ try
51+ {
52+ var dividendPerShareConverted = await currencyExchange . ConvertMoney (
53+ item . Dividend . Amount ,
54+ primaryCurrency ,
55+ item . Dividend . ExDividendDate ) ;
56+ dividendPerSharePrimaryCurrency = dividendPerShareConverted . Amount ;
57+ expectedAmountPrimaryCurrency = dividendPerSharePrimaryCurrency * quantity ;
58+ }
59+ catch
60+ {
61+ // Fallback: conversion failed, do not claim value is in primary currency
62+ dividendPerSharePrimaryCurrency = null ;
63+ expectedAmountPrimaryCurrency = null ;
64+ primaryCurrencyLabel = nativeCurrency ;
65+ }
9566
9667 result . Add ( new UpcomingDividendModel
9768 {
9869 Symbol = symbol ,
9970 CompanyName = companyName ,
10071 ExDate = DateTime . SpecifyKind ( item . Dividend . ExDividendDate . ToDateTime ( TimeOnly . MinValue ) , DateTimeKind . Utc ) ,
10172 PaymentDate = DateTime . SpecifyKind ( item . Dividend . PaymentDate . ToDateTime ( TimeOnly . MinValue ) , DateTimeKind . Utc ) ,
102-
103- // Native currency (original dividend currency)
10473 Amount = expectedAmount ,
10574 Currency = nativeCurrency ,
10675 DividendPerShare = dividendPerShare ,
107-
108- // Primary currency equivalent
10976 AmountPrimaryCurrency = expectedAmountPrimaryCurrency ,
110- PrimaryCurrency = primaryCurrency . Symbol ,
77+ PrimaryCurrency = primaryCurrencyLabel ,
11178 DividendPerSharePrimaryCurrency = dividendPerSharePrimaryCurrency ,
112-
11379 Quantity = quantity ,
11480 IsPredicted = item . Dividend . DividendState == DividendState . Predicted
11581 } ) ;
11682 }
11783
11884 return result ;
11985 }
86+
87+ private static async Task < Dictionary < string , decimal > > GetHoldingsDictionaryAsync ( DatabaseContext databaseContext , DateOnly lastKnownDate )
88+ {
89+ var holdingsWithProfiles = await databaseContext . Holdings
90+ . Include ( h => h . SymbolProfiles )
91+ . ToListAsync ( ) ;
92+
93+ var snapshots = await databaseContext . CalculatedSnapshots
94+ . Where ( s => s . Date == lastKnownDate )
95+ . ToListAsync ( ) ;
96+
97+ var snapshotLookup = snapshots
98+ . GroupBy ( s => s . HoldingId )
99+ . ToDictionary ( g => g . Key , g => g . Sum ( s => s . Quantity ) ) ;
100+
101+ return holdingsWithProfiles
102+ . Select ( h => new
103+ {
104+ Symbol = h . SymbolProfiles . FirstOrDefault ( ) ? . Symbol ,
105+ Quantity = snapshotLookup . TryGetValue ( h . Id , out var qty ) ? qty : 0
106+ } )
107+ . Where ( x => ! string . IsNullOrEmpty ( x . Symbol ) && x . Quantity > 0 )
108+ . GroupBy ( x => x . Symbol )
109+ . ToDictionary ( g => g . Key ! , g => g . Sum ( x => x . Quantity ) ) ;
110+ }
111+
112+ private sealed class DividendWithProfile
113+ {
114+ public Dividend Dividend { get ; set ; } = default ! ;
115+ public SymbolProfile SymbolProfile { get ; set ; } = default ! ;
116+ }
117+
118+ private static async Task < List < DividendWithProfile > > GetUpcomingDividendsWithProfilesAsync ( DatabaseContext databaseContext )
119+ {
120+ var today = DateOnly . FromDateTime ( DateTime . Today ) ;
121+ return await databaseContext . Dividends
122+ . Where ( dividend => dividend . PaymentDate >= today )
123+ . Join ( databaseContext . SymbolProfiles ,
124+ dividend => new { Symbol = dividend . SymbolProfileSymbol , DataSource = dividend . SymbolProfileDataSource } ,
125+ symbolProfile => new { Symbol = ( string ? ) symbolProfile . Symbol , DataSource = ( string ? ) symbolProfile . DataSource } ,
126+ ( dividend , symbolProfile ) => new DividendWithProfile { Dividend = dividend , SymbolProfile = symbolProfile } )
127+ . Where ( x => x . Dividend . Amount . Amount > 0 )
128+ . ToListAsync ( ) ;
129+ }
120130 }
121- }
131+ }
0 commit comments