Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ ynab accounts transactions <id>
```bash
ynab categories list
ynab categories view <id>
ynab categories update <id> [--name <name>] [--note <note>] [--category-group-id <id>] [--goal-target <amount>]
ynab categories budget <id> --month <YYYY-MM> --amount <amount>
ynab categories transactions <id>
```
Expand Down
65 changes: 65 additions & 0 deletions src/commands/categories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,71 @@ export function createCategoriesCommand(): Command {
})
);

cmd
.command('update')
.description('Update category details')
.argument('<id>', 'Category ID')
.option('--name <name>', 'New category name')
.option('--note <note>', 'New category note')
.option('--category-group-id <id>', 'Move to a different category group')
.option('--goal-target <amount>', 'Goal target amount (only if goal already exists)', parseFloat)
.option('-b, --budget <id>', '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)')
Expand Down
9 changes: 9 additions & 0 deletions src/lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down