diff --git a/README.md b/README.md index b8f0bad..c703552 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ ynab accounts transactions ```bash ynab categories list ynab categories view +ynab categories update [--name ] [--note ] [--category-group-id ] [--goal-target ] ynab categories budget --month --amount ynab categories transactions ``` diff --git a/src/commands/categories.ts b/src/commands/categories.ts index f8ba596..feea070 100644 --- a/src/commands/categories.ts +++ b/src/commands/categories.ts @@ -36,6 +36,71 @@ export function createCategoriesCommand(): Command { }) ); + cmd + .command('update') + .description('Update category details') + .argument('', 'Category ID') + .option('--name ', 'New category name') + .option('--note ', 'New category note') + .option('--category-group-id ', 'Move to a different category group') + .option('--goal-target ', 'Goal target amount (only if goal already exists)', parseFloat) + .option('-b, --budget ', 'Budget ID') + .action( + withErrorHandling( + async ( + id: string, + options: { + name?: string; + note?: string; + categoryGroupId?: string; + goalTarget?: number; + budget?: string; + } & CommandOptions + ) => { + // Validate at least one field is provided + if (options.name === undefined && options.note === undefined && options.categoryGroupId === undefined && options.goalTarget === undefined) { + throw new YnabCliError( + 'At least one field to update must be provided (--name, --note, --category-group-id, or --goal-target)', + 400 + ); + } + + // Validate name if provided + if (options.name !== undefined && options.name.trim() === '') { + throw new YnabCliError('Category name cannot be empty or whitespace', 400); + } + + // Validate goal-target if provided + if (options.goalTarget !== undefined && isNaN(options.goalTarget)) { + throw new YnabCliError('Goal target must be a valid number', 400); + } + + const updateData: { + name?: string; + note?: string | null; + category_group_id?: string; + goal_target?: number | null; + } = {}; + + if (options.name !== undefined) { + updateData.name = options.name.trim(); + } + if (options.note !== undefined) { + updateData.note = options.note.trim() || null; + } + if (options.categoryGroupId) { + updateData.category_group_id = options.categoryGroupId; + } + if (options.goalTarget !== undefined) { + updateData.goal_target = amountToMilliunits(options.goalTarget); + } + + const category = await client.updateCategory(id, { category: updateData }, options.budget); + outputJson(category); + } + ) + ); + cmd .command('budget') .description('Set category budgeted amount for a month (overrides existing amount)') diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index e95ff12..f6bf6f8 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -159,6 +159,15 @@ export class YnabClient { }); } + async updateCategory(categoryId: string, data: ynab.PatchCategoryWrapper, budgetId?: string) { + return this.withErrorHandling(async () => { + const api = await this.getApi(); + const id = await this.getBudgetId(budgetId); + const response = await api.categories.updateCategory(id, categoryId, data); + return response.data.category; + }); + } + async getPayees(budgetId?: string, lastKnowledgeOfServer?: number) { return this.withErrorHandling(async () => { const api = await this.getApi();