@@ -189,6 +189,32 @@ export async function getCategoryGroupTombstones(): Promise<Map<string, boolean>
189189 return groupTombstoneMap ;
190190}
191191
192+ /**
193+ * Get category mappings (category ID -> merged category ID)
194+ * Maps deleted/merged categories to the categories they were merged into
195+ */
196+ export async function getCategoryMappings ( ) : Promise < Map < string , string > > {
197+ if ( ! db ) {
198+ throw new DatabaseError ( 'Database not loaded. Call initialize() first.' ) ;
199+ }
200+
201+ const categoryMapping = new Map < string , string > ( ) ;
202+ try {
203+ const mappings = query ( 'SELECT id, transferId FROM category_mapping' ) ;
204+ mappings . forEach ( m => {
205+ const oldId = String ( m . id ) ;
206+ const newId = String ( m . transferId ) ;
207+ // Only include actual merges (where id != transferId)
208+ if ( oldId !== newId ) {
209+ categoryMapping . set ( oldId , newId ) ;
210+ }
211+ } ) ;
212+ } catch {
213+ // No category_mapping table, that's okay
214+ }
215+ return categoryMapping ;
216+ }
217+
192218/**
193219 * Get categories from database
194220 */
@@ -295,6 +321,22 @@ async function getTransactions(
295321 categories . map ( c => [ String ( c . id ) , { name : String ( c . name ) , tombstone : c . tombstone === 1 } ] ) ,
296322 ) ;
297323
324+ // Get category mappings for resolving merged categories
325+ let categoryMappings = new Map < string , string > ( ) ;
326+ try {
327+ const mappings = query ( 'SELECT id, transferId FROM category_mapping' ) ;
328+ mappings . forEach ( m => {
329+ const oldId = String ( m . id ) ;
330+ const newId = String ( m . transferId ) ;
331+ // Only include actual merges (where id != transferId)
332+ if ( oldId !== newId ) {
333+ categoryMappings . set ( oldId , newId ) ;
334+ }
335+ } ) ;
336+ } catch {
337+ // No category_mapping table, that's okay
338+ }
339+
298340 const result : Transaction [ ] = [ ] ;
299341
300342 for ( const t of transactions ) {
@@ -314,8 +356,20 @@ async function getTransactions(
314356 const payeeInfo = payeeId && payeeInfoMap ? payeeInfoMap . get ( payeeId ) : undefined ;
315357 const payeeTombstone = payeeInfo ? payeeInfo . tombstone : false ;
316358
317- // Get category info (name and tombstone status)
318- const categoryInfo = t . category ? categoryMap . get ( String ( t . category ) ) : undefined ;
359+ // Resolve category ID through merge chain (handle transitive merges A -> B -> C)
360+ let resolvedCategoryId = t . category ? String ( t . category ) : undefined ;
361+
362+ if ( resolvedCategoryId && categoryMappings . has ( resolvedCategoryId ) ) {
363+ // Follow the merge chain until we reach the final target
364+ const visited = new Set < string > ( ) ; // Prevent infinite loops in case of circular references
365+ while ( categoryMappings . has ( resolvedCategoryId ) && ! visited . has ( resolvedCategoryId ) ) {
366+ visited . add ( resolvedCategoryId ) ;
367+ resolvedCategoryId = categoryMappings . get ( resolvedCategoryId ) ! ;
368+ }
369+ }
370+
371+ // Get category info from the resolved (merged) category
372+ const categoryInfo = resolvedCategoryId ? categoryMap . get ( resolvedCategoryId ) : undefined ;
319373 const categoryName = categoryInfo ? categoryInfo . name : undefined ;
320374 const categoryTombstone = categoryInfo ? categoryInfo . tombstone : false ;
321375
@@ -328,7 +382,7 @@ async function getTransactions(
328382 payee_name : payeeName ,
329383 payee_tombstone : payeeTombstone ,
330384 notes : t . notes ? String ( t . notes ) : undefined ,
331- category : t . category ? String ( t . category ) : undefined ,
385+ category : resolvedCategoryId , // Use resolved category ID
332386 category_name : categoryName ,
333387 category_tombstone : categoryTombstone ,
334388 cleared : t . cleared === 1 || false ,
@@ -465,11 +519,28 @@ export async function getBudgetedAmounts(
465519 'December' ,
466520 ] ;
467521
468- // Convert to structured format
469- const result : Array < { categoryId : string ; month : string ; budgetedAmount : number } > = [ ] ;
522+ // Get category mappings for resolving merged categories
523+ let categoryMappings = new Map < string , string > ( ) ;
524+ try {
525+ const mappings = query ( 'SELECT id, transferId FROM category_mapping' ) ;
526+ mappings . forEach ( m => {
527+ const oldId = String ( m . id ) ;
528+ const newId = String ( m . transferId ) ;
529+ // Only include actual merges (where id != transferId)
530+ if ( oldId !== newId ) {
531+ categoryMappings . set ( oldId , newId ) ;
532+ }
533+ } ) ;
534+ } catch {
535+ // No category_mapping table, that's okay
536+ }
537+
538+ // Resolve category IDs through merge chain and accumulate budgets
539+ // Use a map to combine budgets from merged categories
540+ const budgetMap = new Map < string , Map < string , number > > ( ) ; // resolvedCategoryId -> month -> amount
470541
471542 for ( const row of budgetRows ) {
472- const categoryId = String ( row . category || '' ) ;
543+ let categoryId = String ( row . category || '' ) ;
473544 const monthInt = Number ( row . month || 0 ) ;
474545 const amount = Number ( row . amount || 0 ) ;
475546
@@ -478,6 +549,15 @@ export async function getBudgetedAmounts(
478549 continue ;
479550 }
480551
552+ // Resolve category ID through merge chain (handle transitive merges A -> B -> C)
553+ if ( categoryMappings . has ( categoryId ) ) {
554+ const visited = new Set < string > ( ) ; // Prevent infinite loops
555+ while ( categoryMappings . has ( categoryId ) && ! visited . has ( categoryId ) ) {
556+ visited . add ( categoryId ) ;
557+ categoryId = categoryMappings . get ( categoryId ) ! ;
558+ }
559+ }
560+
481561 // Convert month INTEGER (YYYYMM format) to month name
482562 // Extract month number from YYYYMM (e.g., 202501 -> 01 -> January)
483563 const monthNum = monthInt % 100 ; // Get last 2 digits
@@ -488,13 +568,27 @@ export async function getBudgetedAmounts(
488568 const monthStr = monthNames [ monthNum - 1 ] ;
489569 const budgetedAmount = integerToAmount ( amount ) ;
490570
491- result . push ( {
492- categoryId,
493- month : monthStr ,
494- budgetedAmount,
495- } ) ;
571+ // Accumulate budgets for the same resolved category and month
572+ if ( ! budgetMap . has ( categoryId ) ) {
573+ budgetMap . set ( categoryId , new Map ( ) ) ;
574+ }
575+ const monthMap = budgetMap . get ( categoryId ) ! ;
576+ const currentAmount = monthMap . get ( monthStr ) || 0 ;
577+ monthMap . set ( monthStr , currentAmount + budgetedAmount ) ;
496578 }
497579
580+ // Convert map back to array format
581+ const result : Array < { categoryId : string ; month : string ; budgetedAmount : number } > = [ ] ;
582+ budgetMap . forEach ( ( monthMap , categoryId ) => {
583+ monthMap . forEach ( ( budgetedAmount , month ) => {
584+ result . push ( {
585+ categoryId,
586+ month,
587+ budgetedAmount,
588+ } ) ;
589+ } ) ;
590+ } ) ;
591+
498592 return result ;
499593 } catch ( error ) {
500594 // If budget data fetch fails (e.g., table doesn't exist), return empty array
0 commit comments