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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/billing-integrations/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ tracing-subscriber = { workspace = true }
num-format = { workspace = true }
itertools = { workspace = true }
indicatif = { workspace = true }

[dev-dependencies]
axum = { workspace = true }
20 changes: 20 additions & 0 deletions crates/billing-integrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Billing integrations

Operator CLI for publishing control-plane bills to Stripe and approving collection.
`lib.rs` defines the commands and shared calendar-month parser.

- `publish.rs` classifies bills and creates or refreshes draft invoices. `--dry-run`
performs read-only classification against Stripe. Manual bills always use
`send_invoice`. `--recreate-existing` (alias `--recreate-finalized`) replaces
eligible usage invoices; it does not reissue manual bills.
- `send.rs` checks current Stripe state before approval and again before
finalization. Usage drafts without a payment method can switch to Net 30 with
operator approval. Failed switches are excluded from finalization; incorrectly
configured manual drafts require republishing. A changed collection decision
after approval requires another send run. Per-invoice failures do not prevent
other invoices from completing, but cause the command to exit unsuccessfully.
Errors use structured logging so invoice context is retained in redirected output.
- `stripe_utils.rs` wraps Stripe invoices for metadata access and operator tables.

Both commands accept `--month YYYY-MM` or the legacy `YYYY-MM-01` form.
Run the focused checks with `mise exec -- cargo nextest run -p billing-integrations`.
65 changes: 65 additions & 0 deletions crates/billing-integrations/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,68 @@ impl Cli {
}
}
}

fn parse_month(arg: &str) -> anyhow::Result<chrono::NaiveDate> {
use chrono::Datelike;
let date = chrono::NaiveDate::parse_from_str(
&if arg.len() == 7 {
format!("{arg}-01")
} else {
arg.to_owned()
},
"%Y-%m-%d",
)?;
anyhow::ensure!(date.day() == 1, "expected YYYY-MM or YYYY-MM-01");
Ok(date)
}

#[cfg(test)]
mod tests {
use clap::Parser;

#[test]
fn billing_month_and_recreate_alias() {
assert_eq!(
super::parse_month("2026-08").unwrap(),
chrono::NaiveDate::from_ymd_opt(2026, 8, 1).unwrap()
);
for command in ["publish-invoices", "send-invoices"] {
for month in ["2026-08", "2026-08-01", "2026-08-15", "2026-13", "invalid"] {
let mut args = vec![
"billing-integrations",
command,
"--stripe-api-key",
"test",
"--all-tenants",
"--month",
month,
];
if command == "publish-invoices" {
args.extend(["--connection-string", "test"]);
}
assert_eq!(
super::Cli::try_parse_from(args).is_ok(),
matches!(month, "2026-08" | "2026-08-01"),
"{command}: {month}"
);
}
}
for flag in ["--recreate-existing", "--recreate-finalized"] {
assert!(
super::Cli::try_parse_from([
"billing-integrations",
"publish-invoices",
"--stripe-api-key",
"test",
"--connection-string",
"test",
"--all-tenants",
"--month",
"2026-08",
flag
])
.is_ok()
);
}
}
}
Loading
Loading