Files
invoiceninja-sdk/examples/seed_sample_data.rs
T
awkawbandClaude Sonnet 5 fb670cbb08
CI / test (push) Canceled after 0s
CI / Build on the declared MSRV (push) Canceled after 0s
CI / Latest stable (advisory) (push) Canceled after 0s
Rename the crate from invoiceninjars to invoiceninja-sdk
The package name has never matched the repository it lives in
(gitea.awkawb.cloud/awkawb/invoiceninja-sdk, mirrored to
github.com/awkawb1/invoiceninja-sdk) — both pointed at `invoiceninja-sdk`
while `Cargo.toml` declared `invoiceninjars`. Lines everything up on the
name the repo has always used.

Cargo derives the crate's Rust identifier from the package name by
lowercasing hyphens to underscores, so every `use invoiceninjars::...`
becomes `use invoiceninja_sdk::...` — in doctests across `src/`, the four
examples, all four integration test files, and `README.md`. `Cargo.lock`'s
own self-entry picked up the new name on the next `cargo check`, which is
also when the rename gets its real check: every doctest and every test file
has to resolve the new path to compile at all, not just the ones a grep
happens to catch.

`flake.nix`'s `pname` and description follow, so `nix build` produces
`invoiceninja-sdk-0.1.0.drv` rather than a name that no longer matches
anything. `GOALS.md`'s two mentions — the hypothetical CLI's name and the
`cargo install` example — follow too, for the same reason.

Verified clean on both the dev toolchain and the 1.88 MSRV shell: full test
suite (209 tests), clippy, fmt, and `nix build`/`nix flake check`.

Not yet published to crates.io (checked: the name 404s there), so this
lands before the old name was ever claimed — no one's `Cargo.toml` breaks
today, but it's recorded as a Changed entry since `GOALS.md` asks for
breaking changes to be legible in the changelog rather than discovered at
compile time, and the crate is reachable as a git dependency regardless of
crates.io status.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-25 09:43:05 -04:00

233 lines
6.7 KiB
Rust

//! Fills a **disposable** Invoice Ninja instance with one record of each
//! common kind, so `capture_shapes` has real rows to take shapes from.
//!
//! ```sh
//! IN_SMOKE_WRITE_HOST=localhost IN_BASE_URL=http://localhost:8080/api/v1 \
//! IN_API_TOKEN=... cargo run --example seed_sample_data
//! ```
//!
//! **Never run this against production.** It creates records, advances
//! number sequences, and may fire webhooks. It uses the smoke test's guard:
//! `IN_SMOKE_WRITE_HOST` must equal the host in `IN_BASE_URL`. Nothing is
//! deleted afterwards; the instance is meant to be thrown away.
//!
//! Each step is independent: one that fails (a validation rule this script
//! did not anticipate) is reported and the rest carry on, with anything that
//! needed it skipped.
use std::future::Future;
use std::process::ExitCode;
use invoiceninja_sdk::{Error, InvoiceNinjaClient};
use serde::de::DeserializeOwned;
use serde_json::{json, Value};
fn model<T: DeserializeOwned>(value: Value) -> T {
serde_json::from_value(value).expect("seed body matches the model")
}
fn describe(error: &Error) -> String {
match error {
Error::Api { errors, .. } if !errors.is_empty() => format!("{error} {errors:?}"),
_ => error.to_string(),
}
}
/// Runs one creation, reporting the outcome and returning the new id.
async fn step<T>(name: &str, call: impl Future<Output = invoiceninja_sdk::Result<T>>) -> Option<T> {
match call.await {
Ok(created) => {
println!(" created {name}");
Some(created)
}
Err(e) => {
println!(" FAILED {name}: {}", describe(&e));
None
}
}
}
#[tokio::main]
async fn main() -> ExitCode {
let base_url = std::env::var("IN_BASE_URL").expect("set IN_BASE_URL");
let token = std::env::var("IN_API_TOKEN").expect("set IN_API_TOKEN");
let host = reqwest::Url::parse(&base_url)
.ok()
.and_then(|u| u.host_str().map(str::to_string))
.unwrap_or_default();
if std::env::var("IN_SMOKE_WRITE_HOST").as_deref() != Ok(host.as_str()) {
eprintln!("refusing to write: set IN_SMOKE_WRITE_HOST={host} to confirm this instance is disposable");
return ExitCode::FAILURE;
}
let c = InvoiceNinjaClient::new(&base_url, token).expect("client");
println!("seeding {host}");
let line =
json!([{ "product_key": "seed-product", "quantity": 1, "cost": 10.0, "notes": "seed" }]);
let client_id = step(
"client",
c.create_client(&model(json!({
"name": "seed-client",
"contacts": [{ "first_name": "Seed", "last_name": "Contact", "email": "seed@example.com" }],
}))),
)
.await
.and_then(|x| x.id);
let vendor_id = step(
"vendor",
c.create_vendor(&model(json!({ "name": "seed-vendor" }))),
)
.await
.and_then(|x| x.id);
step(
"product",
c.create_product(&model(
json!({ "product_key": "seed-product", "price": 10.0 }),
)),
)
.await;
step(
"tax_rate",
c.create_tax_rate(&model(json!({ "name": "seed-tax", "rate": 5.0 }))),
)
.await;
step(
"payment_term",
c.create_payment_term(&model(json!({ "name": "seed-term", "num_days": 15 }))),
)
.await;
step(
"expense_category",
c.create_expense_category(&model(json!({ "name": "seed-category" }))),
)
.await;
step(
"tag",
c.create_tag(&model(
json!({ "name": "seed-tag", "entity_type": "client" }),
)),
)
.await;
step(
"group_setting",
c.create_group_setting(&model(json!({ "name": "seed-group" }))),
)
.await;
step(
"webhook",
c.create_webhook(&model(
json!({ "event_id": "1", "target_url": "https://example.com/seed-hook" }),
)),
)
.await;
step(
"expense",
c.create_expense(&model(
json!({ "amount": 5.0, "public_notes": "seed", "vendor_id": vendor_id }),
)),
)
.await;
step(
"recurring_expense",
c.create_recurring_expense(&model(
json!({ "amount": 5.0, "frequency_id": 5, "vendor_id": vendor_id }),
)),
)
.await;
if let Some(vendor_id) = &vendor_id {
step(
"purchase_order",
c.create_purchase_order(&model(
json!({ "vendor_id": vendor_id, "line_items": line }),
)),
)
.await;
}
let Some(client_id) = client_id else {
println!("no client, so the client-dependent records are skipped");
return ExitCode::FAILURE;
};
step(
"location",
c.create_location(&model(
json!({ "name": "seed-location", "client_id": client_id, "address1": "1 Seed St" }),
)),
)
.await;
step(
"project",
c.create_project(&model(
json!({ "name": "seed-project", "client_id": client_id }),
)),
)
.await;
step(
"task",
c.create_task(&model(
json!({ "description": "seed task", "client_id": client_id }),
)),
)
.await;
step(
"quote",
c.create_quote(&model(
json!({ "client_id": client_id, "line_items": line }),
)),
)
.await;
step(
"credit",
c.create_credit(&model(
json!({ "client_id": client_id, "line_items": line }),
)),
)
.await;
step(
"recurring_invoice",
c.create_recurring_invoice(&model(
json!({ "client_id": client_id, "frequency_id": 5, "line_items": line }),
)),
)
.await;
// Expected to 500 on 5.13.50: the `recurring_quotes` table has no
// `design_id` column, so the server's own insert fails. Not an SDK bug.
step(
"recurring_quote",
c.create_recurring_quote(&model(
json!({ "client_id": client_id, "frequency_id": 5, "line_items": line }),
)),
)
.await;
let invoice = step(
"invoice",
c.create_invoice(&model(
json!({ "client_id": client_id, "line_items": line }),
)),
)
.await;
if let Some(id) = invoice.and_then(|i| i.id) {
// A payment can only be applied to an invoice that has been sent.
if step("invoice mark_sent", c.mark_invoice_sent(&id))
.await
.is_some()
{
step(
"payment",
c.create_payment(&model(json!({
"client_id": client_id,
"amount": 5.0,
"invoices": [{ "invoice_id": id, "amount": 5.0 }],
}))),
)
.await;
}
}
ExitCode::SUCCESS
}