Add the remaining 22 simple-CRUD resources
Finishes the mechanical rollout: every resource using resource_methods!/crud_methods! in the SDK (list/get/create/update/ delete, no bulk/uploads/entity-actions) is now wired up, 33 in total. recurring_quotes.rs mirrors recurring_invoices.rs the same way quotes.rs mirrors invoices.rs, since RecurringQuote is also a type alias for RecurringInvoice in the SDK. client_gateway_tokens, companies, and system_logs use the SDK's crud_methods! rather than resource_methods! (no /bulk route in Invoice Ninja for these) - doesn't change anything here, since this CLI doesn't expose bulk actions yet either way. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+12
-9
@@ -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.
|
||||
|
||||
@@ -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/<resource>.rs` follows (33 of them now;
|
||||
see "Adding a resource" below).
|
||||
|
||||
### The `update` fetch-merge-write pattern
|
||||
|
||||
|
||||
@@ -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 <id>
|
||||
```
|
||||
|
||||
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 <id>`.
|
||||
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 <id>`. Run `invoiceninja-cli --help`
|
||||
for the full list.
|
||||
|
||||
+48
-3
@@ -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)]
|
||||
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_bank_integration(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
bank_integrations: &[BankIntegration],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_bank_transaction_rule(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
bank_transaction_rules: &[BankTransactionRule],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_bank_transaction(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
bank_transactions: &[BankTransaction],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_client_gateway_token(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
client_gateway_tokens: &[ClientGatewayToken],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_company(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(companies: &[Company], format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_company_gateway(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
company_gateways: &[CompanyGateway],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_design(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(designs: &[Design], format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_expense_category(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
expense_categories: &[ExpenseCategory],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_group_setting(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
group_settings: &[GroupSetting],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_location(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(locations: &[Location], format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_payment_term(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
payment_terms: &[PaymentTerm],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_recurring_expense(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
recurring_expenses: &[RecurringExpense],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_recurring_invoice(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
recurring_invoices: &[RecurringInvoice],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_recurring_quote(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
recurring_quotes: &[RecurringQuote],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_subscription(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
subscriptions: &[Subscription],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_system_log(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
system_logs: &[SystemLog],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_tag(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(tags: &[Tag], format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_task_scheduler(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
task_schedulers: &[TaskScheduler],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_task_status(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(
|
||||
task_statuses: &[TaskStatus],
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_tax_rate(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(tax_rates: &[TaxRate], format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_token(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(tokens: &[CompanyToken], format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
@@ -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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
client.delete_webhook(id).await?;
|
||||
println!("deleted {id}");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(webhooks: &[Webhook], format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
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::<String>() + "…"
|
||||
}
|
||||
}
|
||||
+50
@@ -34,5 +34,55 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user