Add entity action and PDF download to invoices/quotes/credits/purchase-orders
These four resources are the only ones with the SDK's entity_action_methods! route. Adds: - `action <id> <action>` - mark_sent, mark_paid, archive, restore, clone_to_invoice, clone_to_quote, auto_bill, delete (routed), returning the updated record. - `download <id>...` - the rendered PDF, to stdout or `--out <path>`; more than one id streams a single merged PDF (Invoice Ninja's bulk download action). New EntityResourceArgs/EntityAction in cli.rs rather than extending the existing ResourceArgs/Action: Rust enums don't compose, and every resource's `run` match on Action needs to stay exhaustive without a catch-all arm for variants it doesn't support. Found and fixed along the way: the Download variant's --output <path> field collided by name with the global --output table|json flag, causing clap to panic at runtime trying to downcast the wrong type. Renamed to --out. Extended tests/nixos-test.nix to exercise action/download for real against the VM. That caught a test-ordering bug of its own - asserting mark_sent on an invoice *after* paying it off, which correctly doesn't revert status_id from paid back to sent - fixed by reordering the subtests. nix flake check passes clean. Also backfills two CHANGELOG.md gaps from earlier sessions: the VM test and CI additions had never actually been logged there. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+39
-7
@@ -3,24 +3,29 @@
|
||||
//! (Invoice Ninja stores credits in the same table/schema, distinguished by
|
||||
//! an `entity_type`), same as `quotes.rs`.
|
||||
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use invoiceninja_sdk::models::{Credit, ListParams};
|
||||
use invoiceninja_sdk::InvoiceNinjaClient;
|
||||
|
||||
use crate::body;
|
||||
use crate::cli::{Action, BodyArgs, ListArgs, OutputFormat, ResourceArgs};
|
||||
use crate::cli::{BodyArgs, EntityAction, EntityResourceArgs, ListArgs, OutputFormat};
|
||||
use crate::output;
|
||||
|
||||
pub async fn run(
|
||||
client: &InvoiceNinjaClient,
|
||||
args: ResourceArgs,
|
||||
args: EntityResourceArgs,
|
||||
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,
|
||||
EntityAction::List(list_args) => list(client, list_args, format).await,
|
||||
EntityAction::Get { id } => get(client, &id, format).await,
|
||||
EntityAction::Create(body_args) => create(client, body_args, format).await,
|
||||
EntityAction::Update { id, body } => update(client, &id, body, format).await,
|
||||
EntityAction::Delete { id } => delete(client, &id).await,
|
||||
EntityAction::Action { id, action } => do_action(client, &id, &action, format).await,
|
||||
EntityAction::Download { ids, out } => download(client, &ids, out).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +106,33 @@ async fn delete(client: &InvoiceNinjaClient, id: &str) -> Result<(), Box<dyn std
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn do_action(
|
||||
client: &InvoiceNinjaClient,
|
||||
id: &str,
|
||||
action: &str,
|
||||
format: OutputFormat,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let updated = client.credit_action(id, action).await?;
|
||||
render(std::slice::from_ref(&updated), format)
|
||||
}
|
||||
|
||||
async fn download(
|
||||
client: &InvoiceNinjaClient,
|
||||
ids: &[String],
|
||||
out: Option<PathBuf>,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let bytes = if ids.len() == 1 {
|
||||
client.download_credit(&ids[0]).await?
|
||||
} else {
|
||||
client.download_credits(ids).await?
|
||||
};
|
||||
match out {
|
||||
Some(path) => std::fs::write(path, bytes)?,
|
||||
None => std::io::stdout().write_all(&bytes)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render(credits: &[Credit], format: OutputFormat) -> Result<(), Box<dyn std::error::Error>> {
|
||||
match format {
|
||||
OutputFormat::Json => output::print_json(credits)?,
|
||||
|
||||
Reference in New Issue
Block a user