@@ -59,7 +59,7 @@ def _normalize_holdings_map(raw: Any) -> Dict[str, Any]:
5959 return holdings
6060
6161
62- def _normalize_account_payload (payload : Dict [str , Any ]) -> Dict [str , Any ]:
62+ def _normalize_account_payload (payload : Dict [str , Any ]) -> Dict [str , Any ]:
6363 account = Account .from_dict (payload )
6464 normalized = account .to_dict ()
6565 extra = _split_extra (
@@ -85,7 +85,25 @@ def _normalize_account_payload(payload: Dict[str, Any]) -> Dict[str, Any]:
8585 return normalized
8686
8787
88- def _normalize_client_payload (payload : Dict [str , Any ]) -> Dict [str , Any ]:
88+ def _account_fingerprint (account : models .Account ) -> str :
89+ payload : Dict [str , Any ] = {
90+ "name" : account .name or "" ,
91+ "account_type" : account .account_type or "" ,
92+ "ownership_type" : account .ownership_type or "" ,
93+ "custodian" : account .custodian or "" ,
94+ "tags" : sorted (list (account .tags or [])),
95+ "tax_settings" : account .tax_settings or {},
96+ "holdings_map" : account .holdings_map or {},
97+ "lots" : account .lots or {},
98+ "manual_holdings" : account .manual_holdings or [],
99+ "extra" : account .extra or {},
100+ "current_value" : float (account .current_value or 0.0 ),
101+ "active_interval" : account .active_interval or "1M" ,
102+ }
103+ return json .dumps (payload , sort_keys = True , separators = ("," , ":" ), default = str )
104+
105+
106+ def _normalize_client_payload (payload : Dict [str , Any ]) -> Dict [str , Any ]:
89107 client = Client .from_dict (payload )
90108 normalized = client .to_dict ()
91109 extra = _split_extra (
@@ -324,6 +342,73 @@ def update_account(self, client_ref: Any, account_ref: Any, payload: Dict[str, A
324342 db .refresh (account )
325343 return self ._account_to_dict (account )
326344
345+ def find_duplicate_accounts (self ) -> Dict [str , Any ]:
346+ self .ensure_schema ()
347+ with _session_scope (self ._db ) as db :
348+ duplicates : List [Dict [str , Any ]] = []
349+ total_duplicates = 0
350+ client_count = 0
351+ for client in db .query (models .Client ).all ():
352+ groups : Dict [str , List [models .Account ]] = {}
353+ for account in client .accounts :
354+ key = _account_fingerprint (account )
355+ groups .setdefault (key , []).append (account )
356+ has_duplicates = False
357+ for accounts in groups .values ():
358+ if len (accounts ) <= 1 :
359+ continue
360+ has_duplicates = True
361+ accounts_sorted = sorted (accounts , key = lambda item : item .id or 0 )
362+ keeper = accounts_sorted [0 ]
363+ dupes = accounts_sorted [1 :]
364+ duplicates .append (
365+ {
366+ "client_id" : client .client_uid or str (client .id ),
367+ "client_name" : client .name or "" ,
368+ "account_name" : keeper .name or "" ,
369+ "account_type" : keeper .account_type or "" ,
370+ "keep_account_id" : keeper .account_uid or str (keeper .id ),
371+ "duplicate_ids" : [
372+ dup .account_uid or str (dup .id ) for dup in dupes
373+ ],
374+ "duplicate_count" : len (dupes ),
375+ }
376+ )
377+ total_duplicates += len (dupes )
378+ if has_duplicates :
379+ client_count += 1
380+ return {
381+ "count" : total_duplicates ,
382+ "clients" : client_count ,
383+ "details" : duplicates ,
384+ }
385+
386+ def remove_duplicate_accounts (self ) -> Dict [str , Any ]:
387+ self .ensure_schema ()
388+ with _session_scope (self ._db ) as db :
389+ removed = 0
390+ client_count = 0
391+ for client in db .query (models .Client ).all ():
392+ groups : Dict [str , List [models .Account ]] = {}
393+ for account in client .accounts :
394+ key = _account_fingerprint (account )
395+ groups .setdefault (key , []).append (account )
396+ removed_for_client = False
397+ for accounts in groups .values ():
398+ if len (accounts ) <= 1 :
399+ continue
400+ accounts_sorted = sorted (accounts , key = lambda item : item .id or 0 )
401+ dupes = accounts_sorted [1 :]
402+ for account in dupes :
403+ db .delete (account )
404+ removed += 1
405+ removed_for_client = True
406+ if removed_for_client :
407+ client_count += 1
408+ if removed :
409+ db .commit ()
410+ return {"removed" : removed , "clients" : client_count }
411+
327412 def _sync_accounts (
328413 self ,
329414 db : Session ,
0 commit comments