Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Breaking: Add support for multiple YNAB budgets #647

Open
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

whatuserever
Copy link
Contributor

@whatuserever whatuserever commented Feb 10, 2025

This PR makes the following changes to support multiple YNAB accounts:

  1. Adds a Ynab budget id column to the config
  2. Changes YnabConfig to also store ynabBudgetId alongside ynabAccountId
  3. Filters the accounts on the Ynab account id column based on the budget selected on that row
  4. Uses the "old" budget id as the default value for Ynab budget id on new rows
  5. Changes logic in ynab.ts to be based on the relevant Ynab budget id

Important

This change will not work with existing configs!
I'm not sure how to support backward compatibility here due to the YnabConfig changes.
If you have any suggestions do let me know.

Note that this PR uses #646 as a base.
I suggest going through the commits one-by-one when reviewing :)

Closes #645

Screenshot

image

Summary by CodeRabbit

  • New Features
    • Enhanced transaction export functionality that now filters and processes transactions on a per-account basis with dynamic budget category initialization.
    • Updated account mapping interface, which now includes budget selection for improved financial account integration.
    • Added a configuration option allowing users to enable a React-based UI for a refined experience.

These changes streamline transaction management and upgrade the account mapping workflow for better usability and integration.

Copy link

coderabbitai bot commented Feb 10, 2025

Walkthrough

The changes update the system to support multiple YNAB budgets by modifying the data structure for account mappings. The updates include new interfaces and function types to handle account-specific transaction exports, along with changes to several methods for budget-specific processing. User interface components are updated to display the new budget identifier field. In addition, test and configuration files are adjusted to reflect the new nested object structure for mapping account numbers to YNAB budget and account IDs.

Changes

Files Change Summary
packages/main/src/backend/commonTypes.ts Added new interface ExportTransactionsForAccountParams and function type ExportTransactionsForAccountFunction; updated YnabConfig mapping property to use nested objects.
packages/main/src/backend/export/outputVendors/ynab/ynab.ts Imported new type; updated function signatures (getTransactions, getYnabCategoryIdFromCategoryName, getYnabCategories) to accept budgetId; added createTransactionsForAccount for account-specific transaction export; modified transaction filtering and category initialization logic.
packages/preload/src/commonTypes.ts Modified YnabConfig mapping structure to use nested objects; added optional useReactUI property to Config interface.
packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx Introduced new AccountMapping interface with ynabBudgetId; updated type alias and table columns to include budget ID; adjusted option filtering in dropdowns.
packages/renderer/src/store/Store.test.tsx
packages/renderer/src/types.tsx
Updated dummy configuration and YnabConfig definition to reflect nested mapping objects.

Sequence Diagram(s)

sequenceDiagram
    participant U as User
    participant F as ExportTransactionsForAccountFunction
    participant M as Mapping Service
    participant Y as YNAB Service
    participant T as Transaction Creator

    U->>F: Request export for specific account
    F->>M: Retrieve mapping for accountNumber
    M-->>F: Return { ynabBudgetId, ynabAccountId }
    F->>Y: getTransactions(ynabBudgetId, startDate)
    Y-->>F: Transactions data
    F->>T: Filter & create transactions
    T-->>F: Return creation result
    F-->>U: Export result delivered
Loading

Possibly related PRs

  • Fix: Added import id for YNAB #626: Relates to the introduction of ExportTransactionsForAccountFunction and similar updates in handling account-specific transaction exports.

Suggested labels

released

Poem

I'm a rabbit, hopping near and far,
Cheering over changes like a little star.
Multiple budgets now dance in our code,
With every mapping perfectly bestowed.
Hoppity-hop, the future is bright,
In YNAB lands, everything's just right!
🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings (Beta)

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (10)
packages/main/src/backend/export/outputVendors/ynab/ynab.ts (5)

43-67: Ensure categories are initialized correctly before use.
In createTransactions, you call await initCategories() only if budgetCategoriesMap is empty. This might skip re-initializing categories if they are outdated or if budgets have changed since the last fetch. Consider adding logic or a timestamp-based check to ensure the map is up to date or forcibly reinitialized if needed.


148-156: Check for missing categories.
You retrieve budgetId from originalTransaction.accountNumber and then call getYnabCategoryIdFromCategoryName(budgetId, categoryName). If budgetCategoriesMap was never populated due to concurrency or timing issues, the function returns null. Ensure downstream logic can handle a null category gracefully.


180-186: Consolidate error handling for missing budget ID.
If the user’s config lacks a valid ynabBudgetId, you throw an error. For better UX, consider a user-friendly fallback or a combined error message describing both missing account ID and missing budget ID.


192-196: Confirm category name collisions are handled.
getYnabCategoryIdFromCategoryName fetches a category by label. If two categories share the same name in the same budget, it will pick the first. Consider clarifying or documenting the expected behavior if multiple categories share a name.


223-243: Separate transaction fetch and filtering logic.
You now filter out existing YNAB transactions by first retrieving them with getTransactions(...). This is fine, but consider splitting into smaller helper methods or caching logic for clarity and testability. Making the function do less (fetch then filter in distinct steps) can streamline future debugging.

packages/main/src/backend/commonTypes.ts (1)

103-111: Improve code discoverability.
ExportTransactionsForAccountParams and ExportTransactionsForAccountFunction are well-defined to handle per-account transactions. It might be helpful to add short JSDoc comments describing real-world usage or how these new types integrate with multi-budget logic, especially for maintainers unfamiliar with the domain.

packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx (3)

50-64: Expose helpful default or hint text.
The new column for ynabBudgetId uses a dropdown, but if the budgets list is empty, the dropdown might be blank. Consider showing a placeholder option or a message if no budgets are available to guide the user.


71-82: Filter accounts by active budget.
When retrieving account options, you filter only active YNAB accounts for the chosen ynabBudgetId. This makes sense, but ensure that closed or deleted YNAB accounts are intentionally excluded, and consider whether a user might need to see them for historical transactions.


127-135: Preserve data consistency on array-to-object transformation.
You correctly build the new mappingObject with both ynabBudgetId and ynabAccountId. Confirm that the row editing logic prevents duplicates or collisions on the same accountNumber. If collisions are possible, you may need an additional check.

packages/renderer/src/store/Store.test.tsx (1)

159-162: LGTM! Consider adding test cases for multiple budgets.

The test data structure correctly implements the new YNAB account mapping format. However, since this PR adds support for multiple budgets, consider adding test cases where accounts map to different budget IDs to ensure the feature works as intended.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3e7c007 and 04a8232.

⛔ Files ignored due to path filters (1)
  • packages/renderer/src/store/__snapshots__/Store.test.tsx.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • packages/main/src/backend/commonTypes.ts (2 hunks)
  • packages/main/src/backend/export/outputVendors/ynab/ynab.ts (9 hunks)
  • packages/preload/src/commonTypes.ts (1 hunks)
  • packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx (4 hunks)
  • packages/renderer/src/store/Store.test.tsx (1 hunks)
  • packages/renderer/src/types.tsx (1 hunks)
🔇 Additional comments (11)
packages/main/src/backend/export/outputVendors/ynab/ynab.ts (5)

22-24: Use descriptive naming or add clarifying comments.
These lines introduce a new YnabCategory type alias and a global budgetCategoriesMap. The naming is clear, but consider adding clarifying comments explaining why budgetCategoriesMap is structured as a nested map keyed by budgetId and then categoryName.


69-130: Improve resilience for unmapped accounts.
The new createTransactionsForAccount function throws an error if the account is unmapped and then logs a progress event. This flow is fine for clarity, but consider whether you want to offer a default fallback or automatically skip the account silently depending on user needs. Also, verify that the logic out of the try/catch always sets the correct budget ID, or else the call to createTransactions might fail silently if a mismatch occurs.


132-134: Validate the budgetId usage.
getTransactions(budgetId, startDate) is now budget-specific, which aligns well with multi-budget support. Double-check any callers of this function to confirm they always pass a valid budgetId, especially for legacy workflows that might assume a single budget.


177-178: Consider fallback for legacy configurations.
Now returning ynabAccountId from an object, but older configurations might only store a single string. If this is a hard-breaking change, confirm you’ve accounted for existing data migrations or user instructions for manual updates.


357-361: Refresh categories from a single budget.
getYnabCategories(budgetId) retrieves category names for the provided budget ID. Ensure that in multi-budget contexts, the UI or caller logic re-calls this function for the correct budget and merges or replaces categories as needed.

packages/main/src/backend/commonTypes.ts (1)

68-68: Breaking change for existing Config.
accountNumbersToYnabAccountIds now maps each string to an object with both ynabBudgetId and ynabAccountId. This is a meaningful schema change, so confirm that existing configurations are migrated or that you have instructions for users to manually update.

packages/renderer/src/components/exporters/YnabAccountMappingTable.tsx (2)

18-25: Clarify required vs. optional fields.
interface AccountMapping includes ynabBudgetId and ynabAccountId, which must be chosen by the user. If there’s a permitted scenario where either is undefined, consider marking them optional or adding validation.


102-103: Initialize new rows with correct default.
ynabBudgetId: budgetId on new rows might not always reflect the user’s actual desired budget, especially if the user wants to fill out multiple budgets. A default is fine, but consider whether to prompt the user to choose from a list if you anticipate more than one valid budget.

packages/preload/src/commonTypes.ts (2)

70-70: LGTM! Interface update aligns with multi-budget support.

The updated accountNumbersToYnabAccountIds type correctly supports the new multi-budget feature by including both budget and account IDs.


235-235: Verify the purpose of the useReactUI property.

This property appears unrelated to the YNAB multi-budget feature. Please clarify its purpose and consider moving it to a separate PR if unrelated.

packages/renderer/src/types.tsx (1)

73-73: LGTM! Consistent interface implementation.

The YnabConfig interface update matches the changes in other files, maintaining type consistency throughout the application.

Comment on lines +70 to +72
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getOptions: (_: any, { row }: { row: AccountMapping }) => {
const budgetId = (row as AccountMapping).ynabBudgetId;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea how to properly type this.
getOptions isn't covered by DefinitelyTyped, the documentation doesn't indicate the type, and the source code is untyped JavaScript. The only hint was the example here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I passed the same way you went through (if I was reading your comment completely before trying to solve this... 😂), and then I found out we don't need to research this.

You can read about this here:
https://stackoverflow.com/a/63254304/839513

We use any to ignore type checking, which may hide errors, and we try to avoid it.

But if you use unknown, it will prevent you from accessing and manipulating the object, which is exactly the situation here, we don't know the type of _ and we don't want to use it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I'll change this tomorrow

@@ -70,7 +70,7 @@ export interface GoogleSheetsConfig extends OutputVendorConfigBase {
export interface YnabConfig extends OutputVendorConfigBase {
options: {
accessToken: string;
accountNumbersToYnabAccountIds: Record<string, string>;
accountNumbersToYnabAccountIds: Record<string, { ynabBudgetId: string; ynabAccountId: string }>;
budgetId: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I'm keeping the "original" budgetId is to pre-populate new rows in the config with it. This way users do not have to reselect a budget for every account if they just use a single budget. This budgetId is not used at all other than that.
However, I think it might be a bit confusing for the user that they have to define a "main" budget Id and an individual Ynab budget id per account.

I can see 2 alternative:

  1. Rename the title in the UI from budget id to default budget id, maybe even add a tooltip that explains it's only used for pre-populating new rows.
  2. Remove it, and use the budget id of the last row (if exists) for pre-populating new rows.
    • This might be troublesome, because in my experience, the rows do not retain their original order on changes.

@whatuserever whatuserever changed the title New: Add support for multiple YNAB budgets Breaking: Add support for multiple YNAB budgets Feb 11, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
packages/main/src/backend/export/outputVendors/ynab/ynab.ts (4)

43-67: Consider adding error handling for Promise.all.

While the parallel execution of transactions is good, if one account fails, it could cause all accounts to fail. Consider handling errors per account to ensure partial success.

-  const results = await Promise.all(promises);
-  const exportedTransactionsNums = results.map((v) => v.exportedTransactionsNum);
+  const results = await Promise.allSettled(promises);
+  const exportedTransactionsNums = results
+    .filter((result): result is PromiseFulfilledResult<{ exportedTransactionsNum: number }> => 
+      result.status === 'fulfilled')
+    .map((result) => result.value.exportedTransactionsNum);

69-130: Enhance error messages for better debugging.

The error messages could be more specific about which step failed during transaction creation.

-    throw new Error(`Unhandled account number ${transactionAccountNumber}`);
+    throw new Error(`Failed to find YNAB mapping for account number ${transactionAccountNumber}. Please check your configuration.`);
-    console.error('Failed to create transactions in ynab', e);
+    console.error(`Failed to create ${transactionsThatDontExistInYnab.length} transactions in YNAB for account ${accountNumber}:`, e);

24-25: Consider implementing cache expiration for transactionsFromYnab.

The transactionsFromYnab Map caches transactions by date but doesn't have an expiration mechanism. This could lead to memory issues over time.

Consider implementing a simple LRU cache or adding an expiration mechanism:

-const transactionsFromYnab = new Map<Date, ynab.TransactionDetail[]>();
+const transactionsFromYnab = new Map<Date, { transactions: ynab.TransactionDetail[], timestamp: number }>();

 if (transactionsFromYnab.has(startDate)) {
-    transactionsInYnabBeforeCreatingTheseTransactions = transactionsFromYnab.get(startDate)!;
+    const cached = transactionsFromYnab.get(startDate)!;
+    if (Date.now() - cached.timestamp < 3600000) { // 1 hour expiration
+      transactionsInYnabBeforeCreatingTheseTransactions = cached.transactions;
+    } else {
+      transactionsFromYnab.delete(startDate);
+    }
 }

Also applies to: 230-237


247-263: Improve type safety and readability in transaction comparison.

  1. The @ts-expect-error suggests a type safety issue that should be addressed.
  2. The magic number 1000 in the amount comparison could be more readable with a named constant.
+const AMOUNT_COMPARISON_THRESHOLD = 1000; // Represents 1 unit in YNAB's milliunits
+
 export function isSameTransaction(
   transactionToCreate: ynab.SaveTransaction,
   transactionFromYnab: ynab.TransactionDetail,
 ) {
   const isATransferTransaction = !!transactionFromYnab.transfer_account_id;
   const isTransactionsImportIdEqual = isSameImportId(transactionToCreate, transactionFromYnab);
+  const transactionAmount = transactionToCreate.amount ?? 0;
   return (
     transactionToCreate.account_id === transactionFromYnab.account_id &&
     transactionToCreate.date === transactionFromYnab.date &&
-    // @ts-expect-error error TS18049: 'transactionToCreate.amount' is possibly 'null' or 'undefined'
-    Math.abs(transactionToCreate.amount - transactionFromYnab.amount) < 1000 &&
+    Math.abs(transactionAmount - transactionFromYnab.amount) < AMOUNT_COMPARISON_THRESHOLD &&
     (areStringsEqualIgnoreCaseAndWhitespace(transactionToCreate.payee_name, transactionFromYnab.payee_name) ||
       isATransferTransaction ||
       isTransactionsImportIdEqual)
   );
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04a8232 and bb3a7e7.

📒 Files selected for processing (1)
  • packages/main/src/backend/export/outputVendors/ynab/ynab.ts (9 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: build (windows-latest)
  • GitHub Check: build (macos-13)
🔇 Additional comments (1)
packages/main/src/backend/export/outputVendors/ynab/ynab.ts (1)

1-25: LGTM! Well-structured type definitions and data structures.

The new type definitions and the introduction of budgetCategoriesMap provide a clean foundation for handling multiple budgets.

@@ -70,7 +70,7 @@ export interface GoogleSheetsConfig extends OutputVendorConfigBase {
export interface YnabConfig extends OutputVendorConfigBase {
options: {
accessToken: string;
accountNumbersToYnabAccountIds: Record<string, string>;
accountNumbersToYnabAccountIds: Record<string, { ynabBudgetId: string; ynabAccountId: string }>;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of declaring a Record, you can declare an object:

accountNumbersToYnabAccountIds: {
    [budgetId: string]: {
        ynabBudgetId: string;
        ynabAccountId: string
    }
}

I'm using this structure when it is not clear what is the key, and this give me the option to give a name to the key.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, will do.

btw, the key is accountNumber, not budgetId. Just further emphasizing your point 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linter does not allow this :/

error  A record is preferred over an index signature  @typescript-eslint/consistent-indexed-object-style

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can do type AccountNumber = string and Record<AccountNumber, ...> if you find that better

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feature Request: Support multiple YNAB budgets
2 participants