diff --git a/CHANGELOG.md b/CHANGELOG.md index e28ff94..0bf8cc3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,15 +12,18 @@ command line. ### Added -- Eleven resources, each with `list` (with `--all` for every page, - `--status`, and `--param key=value` for anything `ListParams` doesn't have - a typed setter for), `get`, `create`, `update`, and `delete`: `clients`, - `invoices`, `quotes`, `credits`, `payments`, `products`, `expenses`, - `projects`, `tasks`, `vendors`, `purchase-orders`. `quotes` and `credits` - are `invoices` under different endpoints — Invoice Ninja stores all three - in the same table, distinguished by `entity_type`, so `Quote` and `Credit` - are type aliases for `Invoice` in the SDK and `commands/quotes.rs` / - `commands/credits.rs` are otherwise identical to `commands/invoices.rs`. +- 33 resources, every one of them with a simple `list`/`get`/`create`/ + `update`/`delete` API in the SDK (`resource_methods!`/`crud_methods!`, no + bulk/uploads/entity-actions) — see `invoiceninja-cli --help` for the + full list. `list` takes `--all` for every page, `--status`, and + `--param key=value` for anything `ListParams` doesn't have a typed + setter for. `quotes` and `credits` are `invoices` under different + endpoints — Invoice Ninja stores all three in the same table, + distinguished by `entity_type`, so `Quote` and `Credit` are type aliases + for `Invoice` in the SDK and `commands/quotes.rs`/`commands/credits.rs` + are otherwise identical to `commands/invoices.rs`. Likewise + `recurring_quotes`/`RecurringQuote` mirror `recurring_invoices`/ + `RecurringInvoice`. - `update` fetches the current record, shallow-merges the caller's `--data`/`--file` JSON on top, and writes the merged result back, rather than sending the patch alone — Invoice Ninja's `PUT` is a full replace. diff --git a/CLAUDE.md b/CLAUDE.md index 08f4968..0821bfd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,11 +20,12 @@ design call, the same way its counterpart in the SDK works. If a change would contradict a stated goal or settle an open question, update `GOALS.md` in the same change. -**Early scaffold.** Eleven resources are wired up (`clients`, `invoices`, -`quotes`, `credits`, `payments`, `products`, `expenses`, `projects`, -`tasks`, `vendors`, `purchase_orders`) out of ~34; `clients` was the -template the other ten followed. See "Adding a resource" below before -writing the next one. +**Scaffold status.** Every resource that fits the simple CRUD template +(`resource_methods!`/`crud_methods!` in the SDK — list/get/create/update/ +delete, no bulk/uploads/entity-actions) is wired up, 33 in total; `clients` +was the template the rest followed. See "Adding a resource" below for what +a resource needing more than the template (bulk actions, uploads, entity +actions, non-CRUD endpoints) will need beyond this. ## Development environment @@ -144,8 +145,9 @@ SDK call. - `src/output.rs` — `print_json`, shared by every resource's `--output json`. Table rendering is **not** here; it's per-resource (see below), since there's no generic notion of which columns matter. -- `src/commands/clients.rs` — the one resource implemented so far, and the - template for the rest. +- `src/commands/clients.rs` — the first resource implemented, and the + template every other `src/commands/.rs` follows (33 of them now; + see "Adding a resource" below). ### The `update` fetch-merge-write pattern diff --git a/README.md b/README.md index 7cc32f8..af5cad7 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ A command-line client for the [Invoice Ninja](https://www.invoiceninja.com/) v5 API, built on [`invoiceninja-sdk`](https://crates.io/crates/invoiceninja-sdk). -> **Early scaffold.** `clients`, `invoices`, `quotes`, `credits`, `payments`, -> `products`, `expenses`, `projects`, `tasks`, `vendors`, and -> `purchase-orders` are wired up so far, out of ~34 resources — see -> `CLAUDE.md` for the pattern the rest follow. [`GOALS.md`](GOALS.md) covers -> what the crate is for and what it is not. +> **Scaffold status.** Every resource with a simple list/get/create/update/ +> delete API (33 of them) is wired up — see `CLAUDE.md` for what's still +> missing (bulk actions, uploads, entity actions, non-CRUD endpoints) and +> the pattern those will follow. [`GOALS.md`](GOALS.md) covers what the +> crate is for and what it is not. ## Development @@ -49,8 +49,8 @@ table view's few key columns: invoiceninja-cli --output json clients get ``` -Every other wired-up resource (`invoices`, `quotes`, `credits`, `payments`, -`products`, `expenses`, `projects`, `tasks`, `vendors`, -`purchase-orders`) follows the same `list`/`get`/`create`/`update`/`delete` -shape, e.g. `invoiceninja-cli invoices list --status active` or -`invoiceninja-cli purchase-orders get `. +Every other wired-up resource follows the same +`list`/`get`/`create`/`update`/`delete` shape, e.g. +`invoiceninja-cli invoices list --status active` or +`invoiceninja-cli purchase-orders get `. Run `invoiceninja-cli --help` +for the full list. diff --git a/src/cli.rs b/src/cli.rs index 433fe76..bd3f6d3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -45,9 +45,10 @@ pub enum OutputFormat { Json, } -/// One variant per resource. Eleven are wired up so far (`clients` was the -/// template for the rest); see CLAUDE.md's "Adding a resource" for the -/// remaining ~23. +/// One variant per resource. Every resource that fits the simple CRUD +/// template (`resource_methods!`/`crud_methods!` in the SDK) is wired up; +/// see CLAUDE.md's "Adding a resource" for what's deliberately left out +/// (bulk actions, uploads, entity actions, non-CRUD endpoints). #[derive(Subcommand)] pub enum Command { /// Manage clients @@ -72,6 +73,50 @@ pub enum Command { Vendors(ResourceArgs), /// Manage purchase orders PurchaseOrders(ResourceArgs), + /// Manage recurring invoices + RecurringInvoices(ResourceArgs), + /// Manage recurring quotes + RecurringQuotes(ResourceArgs), + /// Manage recurring expenses + RecurringExpenses(ResourceArgs), + /// Manage tax rates + TaxRates(ResourceArgs), + /// Manage expense categories + ExpenseCategories(ResourceArgs), + /// Manage task statuses + TaskStatuses(ResourceArgs), + /// Manage payment terms + PaymentTerms(ResourceArgs), + /// Manage subscriptions + Subscriptions(ResourceArgs), + /// Manage bank transactions + BankTransactions(ResourceArgs), + /// Manage webhooks + Webhooks(ResourceArgs), + /// Manage bank integrations + BankIntegrations(ResourceArgs), + /// Manage bank transaction rules + BankTransactionRules(ResourceArgs), + /// Manage company gateways + CompanyGateways(ResourceArgs), + /// Manage designs + Designs(ResourceArgs), + /// Manage group settings + GroupSettings(ResourceArgs), + /// Manage locations + Locations(ResourceArgs), + /// Manage tags + Tags(ResourceArgs), + /// Manage task schedulers + TaskSchedulers(ResourceArgs), + /// Manage API tokens + Tokens(ResourceArgs), + /// Manage client gateway tokens + ClientGatewayTokens(ResourceArgs), + /// Manage companies + Companies(ResourceArgs), + /// Manage system logs + SystemLogs(ResourceArgs), } #[derive(Args)] diff --git a/src/commands/bank_integrations.rs b/src/commands/bank_integrations.rs new file mode 100644 index 0000000..7ed0c25 --- /dev/null +++ b/src/commands/bank_integrations.rs @@ -0,0 +1,140 @@ +//! `bank-integrations` subcommand. Same shape as `clients.rs` — see the +//! "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{BankIntegration, ListParams}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let bank_integrations = if args.all { + client.list_all_bank_integrations(¶ms).await? + } else { + client.list_bank_integrations(¶ms).await? + }; + + render(&bank_integrations, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_bank_integration(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: BankIntegration = serde_json::from_value(value)?; + let created = client.create_bank_integration(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_bank_integration(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: BankIntegration = serde_json::from_value(merged)?; + let updated = client.update_bank_integration(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_bank_integration(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + bank_integrations: &[BankIntegration], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(bank_integrations)?, + OutputFormat::Table => print_table(bank_integrations), + } + Ok(()) +} + +fn print_table(bank_integrations: &[BankIntegration]) { + if bank_integrations.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<20} {:<15} {:>12} {:<8}", + "id", "bank_account_name", "type", "balance", "currency" + ); + for b in bank_integrations { + println!( + "{:<20} {:<20} {:<15} {:>12.2} {:<8}", + b.id.as_deref().unwrap_or(""), + truncate(&b.bank_account_name, 20), + truncate(&b.bank_account_type, 15), + b.balance, + b.currency + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/bank_transaction_rules.rs b/src/commands/bank_transaction_rules.rs new file mode 100644 index 0000000..4825b46 --- /dev/null +++ b/src/commands/bank_transaction_rules.rs @@ -0,0 +1,139 @@ +//! `bank-transaction-rules` subcommand. Same shape as `clients.rs` — see +//! the "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{BankTransactionRule, ListParams}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let bank_transaction_rules = if args.all { + client.list_all_bank_transaction_rules(¶ms).await? + } else { + client.list_bank_transaction_rules(¶ms).await? + }; + + render(&bank_transaction_rules, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_bank_transaction_rule(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: BankTransactionRule = serde_json::from_value(value)?; + let created = client.create_bank_transaction_rule(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_bank_transaction_rule(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: BankTransactionRule = serde_json::from_value(merged)?; + let updated = client.update_bank_transaction_rule(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_bank_transaction_rule(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + bank_transaction_rules: &[BankTransactionRule], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(bank_transaction_rules)?, + OutputFormat::Table => print_table(bank_transaction_rules), + } + Ok(()) +} + +fn print_table(bank_transaction_rules: &[BankTransactionRule]) { + if bank_transaction_rules.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<30} {:<15} {:<8}", + "id", "name", "applies_to", "auto" + ); + for r in bank_transaction_rules { + println!( + "{:<20} {:<30} {:<15} {:<8}", + r.id.as_deref().unwrap_or(""), + truncate(&r.name, 30), + truncate(&r.applies_to, 15), + r.auto_convert + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/bank_transactions.rs b/src/commands/bank_transactions.rs new file mode 100644 index 0000000..3bea2b1 --- /dev/null +++ b/src/commands/bank_transactions.rs @@ -0,0 +1,140 @@ +//! `bank-transactions` subcommand. Same shape as `clients.rs` — see the +//! "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{BankTransaction, ListParams}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let bank_transactions = if args.all { + client.list_all_bank_transactions(¶ms).await? + } else { + client.list_bank_transactions(¶ms).await? + }; + + render(&bank_transactions, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_bank_transaction(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: BankTransaction = serde_json::from_value(value)?; + let created = client.create_bank_transaction(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_bank_transaction(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: BankTransaction = serde_json::from_value(merged)?; + let updated = client.update_bank_transaction(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_bank_transaction(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + bank_transactions: &[BankTransaction], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(bank_transactions)?, + OutputFormat::Table => print_table(bank_transactions), + } + Ok(()) +} + +fn print_table(bank_transactions: &[BankTransaction]) { + if bank_transactions.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<12} {:<30} {:>12} {:<8}", + "id", "date", "description", "amount", "type" + ); + for t in bank_transactions { + println!( + "{:<20} {:<12} {:<30} {:>12.2} {:<8}", + t.id.as_deref().unwrap_or(""), + t.date, + truncate(&t.description, 30), + t.amount, + t.base_type + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/client_gateway_tokens.rs b/src/commands/client_gateway_tokens.rs new file mode 100644 index 0000000..bf09f0d --- /dev/null +++ b/src/commands/client_gateway_tokens.rs @@ -0,0 +1,142 @@ +//! `client-gateway-tokens` subcommand. Same shape as `clients.rs` — see +//! the "Adding a resource" section in CLAUDE.md. Uses the SDK's +//! `crud_methods!` rather than `resource_methods!` (no `/bulk` route), which +//! doesn't change anything here — this CLI doesn't expose bulk actions yet +//! either way. + +use invoiceninja_sdk::models::{ClientGatewayToken, ListParams}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let client_gateway_tokens = if args.all { + client.list_all_client_gateway_tokens(¶ms).await? + } else { + client.list_client_gateway_tokens(¶ms).await? + }; + + render(&client_gateway_tokens, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_client_gateway_token(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: ClientGatewayToken = serde_json::from_value(value)?; + let created = client.create_client_gateway_token(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_client_gateway_token(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: ClientGatewayToken = serde_json::from_value(merged)?; + let updated = client.update_client_gateway_token(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_client_gateway_token(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + client_gateway_tokens: &[ClientGatewayToken], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(client_gateway_tokens)?, + OutputFormat::Table => print_table(client_gateway_tokens), + } + Ok(()) +} + +fn print_table(client_gateway_tokens: &[ClientGatewayToken]) { + if client_gateway_tokens.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<20} {:<20} {:<10}", + "id", "client_id", "company_gateway_id", "is_default" + ); + for t in client_gateway_tokens { + println!( + "{:<20} {:<20} {:<20} {:<10}", + t.id.as_deref().unwrap_or(""), + truncate(&t.client_id, 20), + truncate(&t.company_gateway_id, 20), + t.is_default + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/companies.rs b/src/commands/companies.rs new file mode 100644 index 0000000..cf3118d --- /dev/null +++ b/src/commands/companies.rs @@ -0,0 +1,138 @@ +//! `companies` subcommand. Same shape as `clients.rs` — see the "Adding a +//! resource" section in CLAUDE.md. Uses the SDK's `crud_methods!` rather +//! than `resource_methods!` (no `/bulk` route), which doesn't change +//! anything here — this CLI doesn't expose bulk actions yet either way. + +use invoiceninja_sdk::models::{Company, ListParams}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let companies = if args.all { + client.list_all_companies(¶ms).await? + } else { + client.list_companies(¶ms).await? + }; + + render(&companies, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_company(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: Company = serde_json::from_value(value)?; + let created = client.create_company(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_company(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: Company = serde_json::from_value(merged)?; + let updated = client.update_company(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_company(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render(companies: &[Company], format: OutputFormat) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(companies)?, + OutputFormat::Table => print_table(companies), + } + Ok(()) +} + +fn print_table(companies: &[Company]) { + if companies.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<20} {:<20} {:<10}", + "id", "subdomain", "portal_domain", "disabled" + ); + for c in companies { + println!( + "{:<20} {:<20} {:<20} {:<10}", + c.id.as_deref().unwrap_or(""), + truncate(&c.subdomain, 20), + truncate(&c.portal_domain, 20), + c.is_disabled + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/company_gateways.rs b/src/commands/company_gateways.rs new file mode 100644 index 0000000..98d9c10 --- /dev/null +++ b/src/commands/company_gateways.rs @@ -0,0 +1,139 @@ +//! `company-gateways` subcommand. Same shape as `clients.rs` — see the +//! "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{CompanyGateway, ListParams}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let company_gateways = if args.all { + client.list_all_company_gateways(¶ms).await? + } else { + client.list_company_gateways(¶ms).await? + }; + + render(&company_gateways, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_company_gateway(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: CompanyGateway = serde_json::from_value(value)?; + let created = client.create_company_gateway(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_company_gateway(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: CompanyGateway = serde_json::from_value(merged)?; + let updated = client.update_company_gateway(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_company_gateway(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + company_gateways: &[CompanyGateway], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(company_gateways)?, + OutputFormat::Table => print_table(company_gateways), + } + Ok(()) +} + +fn print_table(company_gateways: &[CompanyGateway]) { + if company_gateways.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<20} {:>12} {:<10}", + "id", "gateway_key", "cards_bitmask", "req_billing" + ); + for g in company_gateways { + println!( + "{:<20} {:<20} {:>12} {:<10}", + g.id.as_deref().unwrap_or(""), + truncate(&g.gateway_key, 20), + g.accepted_credit_cards, + g.require_billing_address + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/designs.rs b/src/commands/designs.rs new file mode 100644 index 0000000..95c64d7 --- /dev/null +++ b/src/commands/designs.rs @@ -0,0 +1,136 @@ +//! `designs` subcommand. Same shape as `clients.rs` — see the "Adding a +//! resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{Design, ListParams}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let designs = if args.all { + client.list_all_designs(¶ms).await? + } else { + client.list_designs(¶ms).await? + }; + + render(&designs, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_design(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: Design = serde_json::from_value(value)?; + let created = client.create_design(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_design(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: Design = serde_json::from_value(merged)?; + let updated = client.update_design(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_design(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render(designs: &[Design], format: OutputFormat) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(designs)?, + OutputFormat::Table => print_table(designs), + } + Ok(()) +} + +fn print_table(designs: &[Design]) { + if designs.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<30} {:<10} {:<10}", + "id", "name", "custom", "active" + ); + for d in designs { + println!( + "{:<20} {:<30} {:<10} {:<10}", + d.id.as_deref().unwrap_or(""), + truncate(&d.name, 30), + d.is_custom, + d.is_active + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/expense_categories.rs b/src/commands/expense_categories.rs new file mode 100644 index 0000000..0fb518a --- /dev/null +++ b/src/commands/expense_categories.rs @@ -0,0 +1,135 @@ +//! `expense-categories` subcommand. Same shape as `clients.rs` — see the +//! "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{ExpenseCategory, ListParams}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let expense_categories = if args.all { + client.list_all_expense_categories(¶ms).await? + } else { + client.list_expense_categories(¶ms).await? + }; + + render(&expense_categories, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_expense_category(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: ExpenseCategory = serde_json::from_value(value)?; + let created = client.create_expense_category(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_expense_category(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: ExpenseCategory = serde_json::from_value(merged)?; + let updated = client.update_expense_category(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_expense_category(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + expense_categories: &[ExpenseCategory], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(expense_categories)?, + OutputFormat::Table => print_table(expense_categories), + } + Ok(()) +} + +fn print_table(expense_categories: &[ExpenseCategory]) { + if expense_categories.is_empty() { + println!("(no records)"); + return; + } + println!("{:<20} {:<30} {:<20}", "id", "name", "user_id"); + for c in expense_categories { + println!( + "{:<20} {:<30} {:<20}", + c.id.as_deref().unwrap_or(""), + truncate(&c.name, 30), + truncate(&c.user_id, 20) + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/group_settings.rs b/src/commands/group_settings.rs new file mode 100644 index 0000000..e0b8eaf --- /dev/null +++ b/src/commands/group_settings.rs @@ -0,0 +1,135 @@ +//! `group-settings` subcommand. Same shape as `clients.rs` — see the +//! "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{GroupSetting, ListParams}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let group_settings = if args.all { + client.list_all_group_settings(¶ms).await? + } else { + client.list_group_settings(¶ms).await? + }; + + render(&group_settings, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_group_setting(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: GroupSetting = serde_json::from_value(value)?; + let created = client.create_group_setting(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_group_setting(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: GroupSetting = serde_json::from_value(merged)?; + let updated = client.update_group_setting(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_group_setting(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + group_settings: &[GroupSetting], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(group_settings)?, + OutputFormat::Table => print_table(group_settings), + } + Ok(()) +} + +fn print_table(group_settings: &[GroupSetting]) { + if group_settings.is_empty() { + println!("(no records)"); + return; + } + println!("{:<20} {:<30} {:<20}", "id", "name", "user_id"); + for g in group_settings { + println!( + "{:<20} {:<30} {:<20}", + g.id.as_deref().unwrap_or(""), + truncate(&g.name, 30), + truncate(&g.user_id, 20) + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/locations.rs b/src/commands/locations.rs new file mode 100644 index 0000000..332b441 --- /dev/null +++ b/src/commands/locations.rs @@ -0,0 +1,137 @@ +//! `locations` subcommand. Same shape as `clients.rs` — see the "Adding a +//! resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{ListParams, Location}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let locations = if args.all { + client.list_all_locations(¶ms).await? + } else { + client.list_locations(¶ms).await? + }; + + render(&locations, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_location(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: Location = serde_json::from_value(value)?; + let created = client.create_location(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_location(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: Location = serde_json::from_value(merged)?; + let updated = client.update_location(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_location(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render(locations: &[Location], format: OutputFormat) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(locations)?, + OutputFormat::Table => print_table(locations), + } + Ok(()) +} + +fn print_table(locations: &[Location]) { + if locations.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<20} {:<20} {:<10} {:<10}", + "id", "name", "city", "state", "country_id" + ); + for l in locations { + println!( + "{:<20} {:<20} {:<20} {:<10} {:<10}", + l.id.as_deref().unwrap_or(""), + truncate(&l.name, 20), + truncate(&l.city, 20), + truncate(&l.state, 10), + l.country_id + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 252fe47..34adc8e 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,11 +1,33 @@ +pub mod bank_integrations; +pub mod bank_transaction_rules; +pub mod bank_transactions; +pub mod client_gateway_tokens; pub mod clients; +pub mod companies; +pub mod company_gateways; pub mod credits; +pub mod designs; +pub mod expense_categories; pub mod expenses; +pub mod group_settings; pub mod invoices; +pub mod locations; +pub mod payment_terms; pub mod payments; pub mod products; pub mod projects; pub mod purchase_orders; pub mod quotes; +pub mod recurring_expenses; +pub mod recurring_invoices; +pub mod recurring_quotes; +pub mod subscriptions; +pub mod system_logs; +pub mod tags; +pub mod task_schedulers; +pub mod task_statuses; pub mod tasks; +pub mod tax_rates; +pub mod tokens; pub mod vendors; +pub mod webhooks; diff --git a/src/commands/payment_terms.rs b/src/commands/payment_terms.rs new file mode 100644 index 0000000..3324239 --- /dev/null +++ b/src/commands/payment_terms.rs @@ -0,0 +1,135 @@ +//! `payment-terms` subcommand. Same shape as `clients.rs` — see the +//! "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{ListParams, PaymentTerm}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let payment_terms = if args.all { + client.list_all_payment_terms(¶ms).await? + } else { + client.list_payment_terms(¶ms).await? + }; + + render(&payment_terms, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_payment_term(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: PaymentTerm = serde_json::from_value(value)?; + let created = client.create_payment_term(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_payment_term(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: PaymentTerm = serde_json::from_value(merged)?; + let updated = client.update_payment_term(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_payment_term(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + payment_terms: &[PaymentTerm], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(payment_terms)?, + OutputFormat::Table => print_table(payment_terms), + } + Ok(()) +} + +fn print_table(payment_terms: &[PaymentTerm]) { + if payment_terms.is_empty() { + println!("(no records)"); + return; + } + println!("{:<20} {:<30} {:>10}", "id", "name", "num_days"); + for t in payment_terms { + println!( + "{:<20} {:<30} {:>10}", + t.id.as_deref().unwrap_or(""), + truncate(&t.name, 30), + t.num_days + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/recurring_expenses.rs b/src/commands/recurring_expenses.rs new file mode 100644 index 0000000..458572a --- /dev/null +++ b/src/commands/recurring_expenses.rs @@ -0,0 +1,140 @@ +//! `recurring-expenses` subcommand. Same shape as `clients.rs` — see the +//! "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{ListParams, RecurringExpense}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let recurring_expenses = if args.all { + client.list_all_recurring_expenses(¶ms).await? + } else { + client.list_recurring_expenses(¶ms).await? + }; + + render(&recurring_expenses, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_recurring_expense(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: RecurringExpense = serde_json::from_value(value)?; + let created = client.create_recurring_expense(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_recurring_expense(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: RecurringExpense = serde_json::from_value(merged)?; + let updated = client.update_recurring_expense(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_recurring_expense(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + recurring_expenses: &[RecurringExpense], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(recurring_expenses)?, + OutputFormat::Table => print_table(recurring_expenses), + } + Ok(()) +} + +fn print_table(recurring_expenses: &[RecurringExpense]) { + if recurring_expenses.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<20} {:<20} {:<12} {:>12}", + "id", "vendor_id", "client_id", "date", "amount" + ); + for e in recurring_expenses { + println!( + "{:<20} {:<20} {:<20} {:<12} {:>12.2}", + e.id.as_deref().unwrap_or(""), + truncate(&e.vendor_id, 20), + truncate(&e.client_id, 20), + e.date, + e.amount + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/recurring_invoices.rs b/src/commands/recurring_invoices.rs new file mode 100644 index 0000000..94e810d --- /dev/null +++ b/src/commands/recurring_invoices.rs @@ -0,0 +1,141 @@ +//! `recurring-invoices` subcommand. Same shape as `clients.rs` — see the +//! "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{ListParams, RecurringInvoice}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let recurring_invoices = if args.all { + client.list_all_recurring_invoices(¶ms).await? + } else { + client.list_recurring_invoices(¶ms).await? + }; + + render(&recurring_invoices, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_recurring_invoice(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: RecurringInvoice = serde_json::from_value(value)?; + let created = client.create_recurring_invoice(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_recurring_invoice(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: RecurringInvoice = serde_json::from_value(merged)?; + let updated = client.update_recurring_invoice(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_recurring_invoice(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + recurring_invoices: &[RecurringInvoice], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(recurring_invoices)?, + OutputFormat::Table => print_table(recurring_invoices), + } + Ok(()) +} + +fn print_table(recurring_invoices: &[RecurringInvoice]) { + if recurring_invoices.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<15} {:<20} {:<8} {:>12} {:>12}", + "id", "number", "client_id", "status", "amount", "balance" + ); + for i in recurring_invoices { + println!( + "{:<20} {:<15} {:<20} {:<8} {:>12.2} {:>12.2}", + i.id.as_deref().unwrap_or(""), + truncate(&i.number, 15), + truncate(&i.client_id, 20), + i.status_id, + i.amount, + i.balance + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/recurring_quotes.rs b/src/commands/recurring_quotes.rs new file mode 100644 index 0000000..ef7181b --- /dev/null +++ b/src/commands/recurring_quotes.rs @@ -0,0 +1,143 @@ +//! `recurring-quotes` subcommand. Same shape as `clients.rs` — see the +//! "Adding a resource" section in CLAUDE.md. `RecurringQuote` is a type +//! alias for `RecurringInvoice` (same schema, distinguished by +//! `entity_type` upstream), same relationship as `quotes.rs`/`invoices.rs`. + +use invoiceninja_sdk::models::{ListParams, RecurringQuote}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let recurring_quotes = if args.all { + client.list_all_recurring_quotes(¶ms).await? + } else { + client.list_recurring_quotes(¶ms).await? + }; + + render(&recurring_quotes, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_recurring_quote(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: RecurringQuote = serde_json::from_value(value)?; + let created = client.create_recurring_quote(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_recurring_quote(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: RecurringQuote = serde_json::from_value(merged)?; + let updated = client.update_recurring_quote(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_recurring_quote(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + recurring_quotes: &[RecurringQuote], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(recurring_quotes)?, + OutputFormat::Table => print_table(recurring_quotes), + } + Ok(()) +} + +fn print_table(recurring_quotes: &[RecurringQuote]) { + if recurring_quotes.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<15} {:<20} {:<8} {:>12} {:>12}", + "id", "number", "client_id", "status", "amount", "balance" + ); + for q in recurring_quotes { + println!( + "{:<20} {:<15} {:<20} {:<8} {:>12.2} {:>12.2}", + q.id.as_deref().unwrap_or(""), + truncate(&q.number, 15), + truncate(&q.client_id, 20), + q.status_id, + q.amount, + q.balance + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/subscriptions.rs b/src/commands/subscriptions.rs new file mode 100644 index 0000000..dddc5e2 --- /dev/null +++ b/src/commands/subscriptions.rs @@ -0,0 +1,140 @@ +//! `subscriptions` subcommand. Same shape as `clients.rs` — see the +//! "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{ListParams, Subscription}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let subscriptions = if args.all { + client.list_all_subscriptions(¶ms).await? + } else { + client.list_subscriptions(¶ms).await? + }; + + render(&subscriptions, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_subscription(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: Subscription = serde_json::from_value(value)?; + let created = client.create_subscription(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_subscription(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: Subscription = serde_json::from_value(merged)?; + let updated = client.update_subscription(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_subscription(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + subscriptions: &[Subscription], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(subscriptions)?, + OutputFormat::Table => print_table(subscriptions), + } + Ok(()) +} + +fn print_table(subscriptions: &[Subscription]) { + if subscriptions.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<20} {:<20} {:<10} {:<10}", + "id", "product_id", "recurring_invoice_id", "frequency", "auto_bill" + ); + for s in subscriptions { + println!( + "{:<20} {:<20} {:<20} {:<10} {:<10}", + s.id.as_deref().unwrap_or(""), + truncate(&s.product_id, 20), + truncate(&s.recurring_invoice_id, 20), + s.frequency_id, + s.auto_bill + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/system_logs.rs b/src/commands/system_logs.rs new file mode 100644 index 0000000..02334f5 --- /dev/null +++ b/src/commands/system_logs.rs @@ -0,0 +1,142 @@ +//! `system-logs` subcommand. Same shape as `clients.rs` — see the "Adding a +//! resource" section in CLAUDE.md. Uses the SDK's `crud_methods!` rather +//! than `resource_methods!` (no `/bulk` route), which doesn't change +//! anything here — this CLI doesn't expose bulk actions yet either way. + +use invoiceninja_sdk::models::{ListParams, SystemLog}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let system_logs = if args.all { + client.list_all_system_logs(¶ms).await? + } else { + client.list_system_logs(¶ms).await? + }; + + render(&system_logs, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_system_log(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: SystemLog = serde_json::from_value(value)?; + let created = client.create_system_log(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_system_log(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: SystemLog = serde_json::from_value(merged)?; + let updated = client.update_system_log(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_system_log(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + system_logs: &[SystemLog], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(system_logs)?, + OutputFormat::Table => print_table(system_logs), + } + Ok(()) +} + +fn print_table(system_logs: &[SystemLog]) { + if system_logs.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<20} {:>10} {:>10} {:>10}", + "id", "client_id", "event_id", "category_id", "type_id" + ); + for l in system_logs { + println!( + "{:<20} {:<20} {:>10} {:>10} {:>10}", + l.id.as_deref().unwrap_or(""), + truncate(&l.client_id, 20), + l.event_id, + l.category_id, + l.type_id + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/tags.rs b/src/commands/tags.rs new file mode 100644 index 0000000..950f020 --- /dev/null +++ b/src/commands/tags.rs @@ -0,0 +1,136 @@ +//! `tags` subcommand. Same shape as `clients.rs` — see the "Adding a +//! resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{ListParams, Tag}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let tags = if args.all { + client.list_all_tags(¶ms).await? + } else { + client.list_tags(¶ms).await? + }; + + render(&tags, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_tag(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: Tag = serde_json::from_value(value)?; + let created = client.create_tag(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_tag(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: Tag = serde_json::from_value(merged)?; + let updated = client.update_tag(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_tag(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render(tags: &[Tag], format: OutputFormat) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(tags)?, + OutputFormat::Table => print_table(tags), + } + Ok(()) +} + +fn print_table(tags: &[Tag]) { + if tags.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<30} {:<15} {:<10}", + "id", "name", "entity_type", "color" + ); + for t in tags { + println!( + "{:<20} {:<30} {:<15} {:<10}", + t.id.as_deref().unwrap_or(""), + truncate(&t.name, 30), + truncate(&t.entity_type, 15), + t.color + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/task_schedulers.rs b/src/commands/task_schedulers.rs new file mode 100644 index 0000000..5cab024 --- /dev/null +++ b/src/commands/task_schedulers.rs @@ -0,0 +1,139 @@ +//! `task-schedulers` subcommand. Same shape as `clients.rs` — see the +//! "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{ListParams, TaskScheduler}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let task_schedulers = if args.all { + client.list_all_task_schedulers(¶ms).await? + } else { + client.list_task_schedulers(¶ms).await? + }; + + render(&task_schedulers, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_task_scheduler(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: TaskScheduler = serde_json::from_value(value)?; + let created = client.create_task_scheduler(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_task_scheduler(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: TaskScheduler = serde_json::from_value(merged)?; + let updated = client.update_task_scheduler(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_task_scheduler(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + task_schedulers: &[TaskScheduler], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(task_schedulers)?, + OutputFormat::Table => print_table(task_schedulers), + } + Ok(()) +} + +fn print_table(task_schedulers: &[TaskScheduler]) { + if task_schedulers.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<20} {:<12} {:<8}", + "id", "job", "repeat_every", "paused" + ); + for s in task_schedulers { + println!( + "{:<20} {:<20} {:<12} {:<8}", + s.id.as_deref().unwrap_or(""), + truncate(&s.job, 20), + s.repeat_every, + s.paused + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/task_statuses.rs b/src/commands/task_statuses.rs new file mode 100644 index 0000000..3efbbef --- /dev/null +++ b/src/commands/task_statuses.rs @@ -0,0 +1,139 @@ +//! `task-statuses` subcommand. Same shape as `clients.rs` — see the +//! "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{ListParams, TaskStatus}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let task_statuses = if args.all { + client.list_all_task_statuses(¶ms).await? + } else { + client.list_task_statuses(¶ms).await? + }; + + render(&task_statuses, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_task_status(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: TaskStatus = serde_json::from_value(value)?; + let created = client.create_task_status(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_task_status(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: TaskStatus = serde_json::from_value(merged)?; + let updated = client.update_task_status(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_task_status(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render( + task_statuses: &[TaskStatus], + format: OutputFormat, +) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(task_statuses)?, + OutputFormat::Table => print_table(task_statuses), + } + Ok(()) +} + +fn print_table(task_statuses: &[TaskStatus]) { + if task_statuses.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<30} {:<10} {:>8}", + "id", "name", "color", "order" + ); + for t in task_statuses { + println!( + "{:<20} {:<30} {:<10} {:>8}", + t.id.as_deref().unwrap_or(""), + truncate(&t.name, 30), + t.color, + t.task_status_order + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/tax_rates.rs b/src/commands/tax_rates.rs new file mode 100644 index 0000000..6195381 --- /dev/null +++ b/src/commands/tax_rates.rs @@ -0,0 +1,132 @@ +//! `tax-rates` subcommand. Same shape as `clients.rs` — see the "Adding a +//! resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{ListParams, TaxRate}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let tax_rates = if args.all { + client.list_all_tax_rates(¶ms).await? + } else { + client.list_tax_rates(¶ms).await? + }; + + render(&tax_rates, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_tax_rate(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: TaxRate = serde_json::from_value(value)?; + let created = client.create_tax_rate(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_tax_rate(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: TaxRate = serde_json::from_value(merged)?; + let updated = client.update_tax_rate(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_tax_rate(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render(tax_rates: &[TaxRate], format: OutputFormat) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(tax_rates)?, + OutputFormat::Table => print_table(tax_rates), + } + Ok(()) +} + +fn print_table(tax_rates: &[TaxRate]) { + if tax_rates.is_empty() { + println!("(no records)"); + return; + } + println!("{:<20} {:<30} {:>10}", "id", "name", "rate"); + for t in tax_rates { + println!( + "{:<20} {:<30} {:>10.2}", + t.id.as_deref().unwrap_or(""), + truncate(&t.name, 30), + t.rate + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/tokens.rs b/src/commands/tokens.rs new file mode 100644 index 0000000..6c9a9dc --- /dev/null +++ b/src/commands/tokens.rs @@ -0,0 +1,132 @@ +//! `tokens` subcommand (API tokens, model `CompanyToken`). Same shape as +//! `clients.rs` — see the "Adding a resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{CompanyToken, ListParams}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let tokens = if args.all { + client.list_all_tokens(¶ms).await? + } else { + client.list_tokens(¶ms).await? + }; + + render(&tokens, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_token(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: CompanyToken = serde_json::from_value(value)?; + let created = client.create_token(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_token(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: CompanyToken = serde_json::from_value(merged)?; + let updated = client.update_token(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_token(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render(tokens: &[CompanyToken], format: OutputFormat) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(tokens)?, + OutputFormat::Table => print_table(tokens), + } + Ok(()) +} + +fn print_table(tokens: &[CompanyToken]) { + if tokens.is_empty() { + println!("(no records)"); + return; + } + println!("{:<20} {:<30} {:<10}", "id", "name", "is_system"); + for t in tokens { + println!( + "{:<20} {:<30} {:<10}", + t.id.as_deref().unwrap_or(""), + truncate(&t.name, 30), + t.is_system + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/commands/webhooks.rs b/src/commands/webhooks.rs new file mode 100644 index 0000000..52af683 --- /dev/null +++ b/src/commands/webhooks.rs @@ -0,0 +1,136 @@ +//! `webhooks` subcommand. Same shape as `clients.rs` — see the "Adding a +//! resource" section in CLAUDE.md. + +use invoiceninja_sdk::models::{ListParams, Webhook}; +use invoiceninja_sdk::InvoiceNinjaClient; + +use crate::body; +use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs}; +use crate::output; + +pub async fn run( + client: &InvoiceNinjaClient, + args: ResourceArgs, + format: OutputFormat, +) -> Result<(), Box> { + match args.action { + Action::List(list_args) => list(client, list_args, format).await, + Action::Get { id } => get(client, &id, format).await, + Action::Create(body_args) => create(client, body_args, format).await, + Action::Update { id, body } => update(client, &id, body, format).await, + Action::Delete { id } => delete(client, &id).await, + } +} + +async fn list( + client: &InvoiceNinjaClient, + args: ListArgs, + format: OutputFormat, +) -> Result<(), Box> { + let mut params = ListParams::new(); + if let Some(page) = args.page { + params = params.page(page); + } + if let Some(per_page) = args.per_page { + params = params.per_page(per_page); + } + if let Some(sort) = args.sort { + params = params.sort(sort); + } + if let Some(filter) = args.filter { + params = params.filter(filter); + } + if let Some(status) = args.status { + params = params.status(status); + } + if let Some(include) = args.include { + params = params.include(include); + } + for (k, v) in args.params { + params = params.param(k, v); + } + + let webhooks = if args.all { + client.list_all_webhooks(¶ms).await? + } else { + client.list_webhooks(¶ms).await? + }; + + render(&webhooks, format) +} + +async fn get( + client: &InvoiceNinjaClient, + id: &str, + format: OutputFormat, +) -> Result<(), Box> { + let found = client.get_webhook(id).await?; + render(std::slice::from_ref(&found), format) +} + +async fn create( + client: &InvoiceNinjaClient, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let value = body::load(&body_args)?; + let model: Webhook = serde_json::from_value(value)?; + let created = client.create_webhook(&model).await?; + render(std::slice::from_ref(&created), format) +} + +async fn update( + client: &InvoiceNinjaClient, + id: &str, + body_args: BodyArgs, + format: OutputFormat, +) -> Result<(), Box> { + let patch = body::load(&body_args)?; + let current = client.get_webhook(id).await?; + let merged = body::merge_shallow(serde_json::to_value(¤t)?, patch); + let model: Webhook = serde_json::from_value(merged)?; + let updated = client.update_webhook(id, &model).await?; + render(std::slice::from_ref(&updated), format) +} + +async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box> { + client.delete_webhook(id).await?; + println!("deleted {id}"); + Ok(()) +} + +fn render(webhooks: &[Webhook], format: OutputFormat) -> Result<(), Box> { + match format { + OutputFormat::Json => output::print_json(webhooks)?, + OutputFormat::Table => print_table(webhooks), + } + Ok(()) +} + +fn print_table(webhooks: &[Webhook]) { + if webhooks.is_empty() { + println!("(no records)"); + return; + } + println!( + "{:<20} {:<10} {:<40} {:<8}", + "id", "event_id", "target_url", "format" + ); + for w in webhooks { + println!( + "{:<20} {:<10} {:<40} {:<8}", + w.id.as_deref().unwrap_or(""), + w.event_id, + truncate(&w.target_url, 40), + w.format + ); + } +} + +fn truncate(s: &str, max: usize) -> String { + if s.chars().count() <= max { + s.to_string() + } else { + s.chars().take(max.saturating_sub(1)).collect::() + "…" + } +} diff --git a/src/main.rs b/src/main.rs index 05ff3e7..4b1236e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,5 +34,55 @@ async fn run() -> Result<(), Box> { Command::PurchaseOrders(args) => { commands::purchase_orders::run(&client, args, cli.output).await } + Command::RecurringInvoices(args) => { + commands::recurring_invoices::run(&client, args, cli.output).await + } + Command::RecurringQuotes(args) => { + commands::recurring_quotes::run(&client, args, cli.output).await + } + Command::RecurringExpenses(args) => { + commands::recurring_expenses::run(&client, args, cli.output).await + } + Command::TaxRates(args) => commands::tax_rates::run(&client, args, cli.output).await, + Command::ExpenseCategories(args) => { + commands::expense_categories::run(&client, args, cli.output).await + } + Command::TaskStatuses(args) => { + commands::task_statuses::run(&client, args, cli.output).await + } + Command::PaymentTerms(args) => { + commands::payment_terms::run(&client, args, cli.output).await + } + Command::Subscriptions(args) => { + commands::subscriptions::run(&client, args, cli.output).await + } + Command::BankTransactions(args) => { + commands::bank_transactions::run(&client, args, cli.output).await + } + Command::Webhooks(args) => commands::webhooks::run(&client, args, cli.output).await, + Command::BankIntegrations(args) => { + commands::bank_integrations::run(&client, args, cli.output).await + } + Command::BankTransactionRules(args) => { + commands::bank_transaction_rules::run(&client, args, cli.output).await + } + Command::CompanyGateways(args) => { + commands::company_gateways::run(&client, args, cli.output).await + } + Command::Designs(args) => commands::designs::run(&client, args, cli.output).await, + Command::GroupSettings(args) => { + commands::group_settings::run(&client, args, cli.output).await + } + Command::Locations(args) => commands::locations::run(&client, args, cli.output).await, + Command::Tags(args) => commands::tags::run(&client, args, cli.output).await, + Command::TaskSchedulers(args) => { + commands::task_schedulers::run(&client, args, cli.output).await + } + Command::Tokens(args) => commands::tokens::run(&client, args, cli.output).await, + Command::ClientGatewayTokens(args) => { + commands::client_gateway_tokens::run(&client, args, cli.output).await + } + Command::Companies(args) => commands::companies::run(&client, args, cli.output).await, + Command::SystemLogs(args) => commands::system_logs::run(&client, args, cli.output).await, } }