flagon-io/g1t

public

Git for AI scale: a forge for thousands of agents working on the same code at once.

g1t/crates/contracts/src/billing.rs

1,346 lines45,310 bytesCodeBlame
//! The billing service: what agents cost, charged to the workspace they
//! worked for.
//!
//! A workspace buys credit and each agent run deducts what it cost, plus
//! g1t's margin. With no credit, no agent starts. Money is held in
//! millionths of a US dollar, so that a run costing a fraction of a cent is
//! recorded exactly.
//!
//! Each `*Args` struct is the argument of the method of the same name,
//! served at `POST /rpc/<method>`.

use serde::{Deserialize, Serialize};

use crate::repos::RepoPath;
use crate::{User, Viewer};

/// Millionths of a US dollar in one dollar.
pub const MICROS_PER_DOLLAR: i64 = 1_000_000;

/// Whether workspaces are charged for agents at all, and with real money.
/// `status` takes nothing and returns this.
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
pub struct Status {
    /// False when no payment provider is configured: nothing is charged,
    /// and who may run agents is decided some other way.
    pub enabled: bool,
    /// False while the payment provider is in its test mode, where cards
    /// are not real.
    pub live: bool,
    /// True while g1t is being built out: runs are recorded, with what
    /// they cost, but nothing is charged and no credit is needed. Not a
    /// promise that it stays free.
    #[serde(default)]
    pub free: bool,
}

/// `trial`: the free allowance on g1t's hosted models for a workspace that
/// is not otherwise open to them, so people can try g1t's agents without a
/// key of their own. Each workspace gets a few dollars of model cost, out
/// of one pool, until an end date. Returns `Trial`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TrialArgs {
    pub workspace: String,
    /// Workspaces open to hosted models anyway, whose use is not counted
    /// against the pool.
    #[serde(default)]
    pub exempt: Vec<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Trial {
    /// Whether its agents may use g1t's hosted models on the allowance now.
    pub open: bool,
    /// What its runs on g1t's models have cost, in millionths of a dollar.
    pub used_micros: i64,
    pub limit_micros: i64,
    /// RFC 3339; when the allowance ends for everyone.
    pub ends_at: Option<String>,
    /// Why it is closed: `off` (no allowance), `ended`, `used` (this
    /// workspace's is spent) or `pool` (everyone's is).
    pub reason: Option<String>,
}

/// A workspace's standing.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Account {
    pub workspace: String,
    /// Credit left, in millionths of a dollar. Can dip below zero by the
    /// cost of the runs that were under way when it ran out.
    pub balance_micros: i64,
    pub status: Status,
    /// What is added to a run's cost, in percent.
    pub margin_percent: u32,
    /// The card g1t charges as the workspace nears its limit and when a
    /// month closes, if one is on file.
    #[serde(default)]
    pub card: Option<Card>,
}

/// A saved card, as far as it is safe to show.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Card {
    /// `visa`, `mastercard`, ...
    pub brand: String,
    pub last4: String,
    pub exp_month: u32,
    pub exp_year: u32,
}

/// `billing_portal`: Stripe's hosted billing page for the workspace, where
/// an owner adds or replaces the card, sees invoices and receipts, and sets
/// the billing email and address. g1t never handles card numbers. Owners
/// only. Returns `Outcome<Checkout>` (its `url`); Stripe sends them back
/// to `return_url`.
#[derive(Debug, Serialize, Deserialize)]
pub struct BillingPortalArgs {
    pub actor: User,
    pub workspace: String,
    pub return_url: String,
}

/// `admin_billing_link`: for staff to send a customer: their Stripe billing
/// page. Returns `Outcome<BillingLink>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminBillingLinkArgs {
    pub workspace: String,
    pub by: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BillingLink {
    /// A one-time session on Stripe's billing page, signed in already.
    pub portal_url: String,
    /// The billing page's sign-in page, which does not expire: the
    /// customer signs in with the email Stripe has for them.
    pub login_url: Option<String>,
    pub customer_email: Option<String>,
    pub expires_note: String,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EntryKind {
    /// Credit bought with a card.
    TopUp,
    /// An agent's run, or a paid feature's usage past its allowance.
    Usage,
}

/// One line of a workspace's statement.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LedgerEntry {
    pub id: String,
    pub kind: EntryKind,
    /// Positive for credit added, negative for usage.
    pub amount_micros: i64,
    pub description: String,
    /// For usage: the repository and pull request the agent worked on.
    pub repo: Option<String>,
    pub number: Option<u32>,
    /// For usage: `implement`, `review` or `update`.
    pub task: Option<String>,
    /// For usage: the model, by its public name.
    pub model: Option<String>,
    /// For usage: `g1t` when g1t paid the model provider, `workspace` when
    /// the workspace's own account did. Runs on the workspace's own
    /// provider pay only their sandbox time now, so only older entries
    /// are `workspace`.
    #[serde(default = "g1t")]
    pub billed_to: String,
    /// For a top-up: the username of whoever paid.
    pub created_by: Option<String>,
    /// RFC 3339.
    pub created_at: String,
    /// The workspace the line belongs to, which tells an enterprise's
    /// lines apart.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub workspace: Option<String>,
}

fn g1t() -> String {
    "g1t".to_owned()
}

/// `account` (`Outcome<Account>`) and `ledger` (`Outcome<Vec<LedgerEntry>>`,
/// newest first). Members of the workspace only.
#[derive(Debug, Serialize, Deserialize)]
pub struct AccountArgs {
    pub workspace: String,
    pub viewer: Viewer,
}

/// `checkout`: starts a card payment for credit. Owners of the workspace
/// only. Returns `Outcome<Checkout>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CheckoutArgs {
    pub actor: User,
    pub workspace: String,
    /// How much credit to buy, in cents.
    pub amount_cents: u32,
    /// Where the payment page sends the person afterwards. The payment's
    /// id is appended as `session`.
    pub return_url: String,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct Checkout {
    /// The payment page to send the person to.
    pub url: String,
}

/// `confirm`: credits a payment once the provider says it was made. Safe
/// to call any number of times. Returns `Outcome<Account>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfirmArgs {
    pub workspace: String,
    pub viewer: Viewer,
    /// The payment's id, as returned to `return_url`.
    pub session: String,
}

/// `can_start`: whether a workspace may start an agent now, asked before
/// anything is opened for it. Returns `Outcome<bool>`: a failure, with the
/// reason to show, when it has no credit.
#[derive(Debug, Serialize, Deserialize)]
pub struct CanStartArgs {
    pub workspace: String,
}

/// `start_run`: asks whether a workspace may start an agent, and opens the
/// run it will be charged for. Called by the runner service. Returns
/// `Outcome<Option<RunTicket>>`: no ticket when billing is off, a failure
/// when the workspace has no credit.
#[derive(Debug, Serialize, Deserialize)]
pub struct StartRunArgs {
    pub workspace: String,
    pub repo: RepoPath,
    pub number: u32,
    /// `implement`, `review` or `update`.
    pub task: String,
    /// The model, by its public name.
    pub model: String,
    /// `workspace` when the run uses the workspace's own model provider.
    /// The runner, which is TypeScript, sends it as `billedTo`.
    #[serde(default = "g1t", alias = "billedTo")]
    pub billed_to: String,
    /// The model session's id, when its requests go through g1t's AI
    /// Gateway: settling charges the run what the gateway priced them at.
    #[serde(default)]
    pub session: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RunTicket {
    pub run_id: String,
    /// Lets the sandbox, and nothing else, report what this run cost.
    pub token: String,
}

/// `finish_run`: what a run cost, as its sandbox reports it. Charged once.
/// Returns `Outcome<bool>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FinishRunArgs {
    pub run_id: String,
    pub token: String,
    /// What the model provider charged, in US dollars.
    pub cost_usd: f64,
    #[serde(default)]
    pub turns: u32,
}


/// `usage`: what a workspace's agents cost over a period, broken down.
/// Members only. Returns `Outcome<Usage>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct UsageArgs {
    pub workspace: String,
    pub viewer: Viewer,
    /// RFC 3339: the start of the period. The period runs to now.
    pub since: String,
}

/// One slice of usage: what it was for, what it cost, how many runs.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UsageSlice {
    pub key: String,
    pub micros: i64,
    pub runs: u32,
}

/// What a workspace's agents cost over a period.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Usage {
    pub since: String,
    /// Charged, including g1t's margin.
    pub spent_micros: i64,
    /// What g1t's model provider charged, before the margin.
    pub cost_micros: i64,
    /// What runs on the workspace's own provider cost there, as the harness
    /// estimated it. Not charged by g1t.
    pub provider_micros: i64,
    /// What the runs used, at cost: g1t's models and the workspace's own
    /// provider together, whatever was charged for them.
    pub used_micros: i64,
    /// g1t charges nothing for now. The slices then measure usage at cost,
    /// since every charge is zero.
    pub free: bool,
    pub runs: u32,
    /// Spend per day (`YYYY-MM-DD`) and task, as `day/task` keys.
    pub by_day: Vec<UsageSlice>,
    /// Per task: implement, review, revise, update, plan.
    pub by_task: Vec<UsageSlice>,
    /// Per repository, `namespace/name`.
    pub by_repo: Vec<UsageSlice>,
    /// The pull requests that cost most, as `namespace/name#number`.
    pub by_pull: Vec<UsageSlice>,
    /// Per model, by its public name.
    pub by_model: Vec<UsageSlice>,
    /// Credit bought in the period.
    pub added_micros: i64,
}

/// A paid feature a workspace turns on with a monthly plan, the way
/// Cloudflare's Workers for Platforms or Vercel's Pro are bought. Never
/// free: `FREE_WHILE_BUILDING` and the free model allowance do not cover
/// it.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Feature {
    /// Previews per pull request and production on g1t.page.
    Deployments,
}

impl Feature {
    pub const ALL: [Feature; 1] = [Feature::Deployments];

    pub fn as_str(self) -> &'static str {
        match self {
            Feature::Deployments => "deployments",
        }
    }

    pub fn parse(name: &str) -> Option<Feature> {
        Feature::ALL.into_iter().find(|feature| feature.as_str() == name)
    }

    pub fn title(self) -> &'static str {
        match self {
            Feature::Deployments => "Deployments",
        }
    }
}

/// What the Deployments plan includes each month; usage past it is charged
/// at cost plus the margin. The billing service describes the plan with
/// these and the deployments service meters against them.
pub mod deployments_allowance {
    /// Apps deployed at once: production and previews together.
    pub const APPS: u32 = 10;
    pub const REQUESTS: u64 = 1_000_000;
    pub const CPU_MS: u64 = 3_000_000;
    /// What Cloudflare charges g1t past that, in millionths of a dollar.
    pub const MICROS_PER_APP_MONTH: i64 = 20_000;
    pub const MICROS_PER_MILLION_REQUESTS: i64 = 300_000;
    pub const MICROS_PER_MILLION_CPU_MS: i64 = 20_000;
    /// What one second of a build's sandbox costs g1t (Cloudflare
    /// Containers, standard-1: half a vCPU, 4 GiB, 8 GB disk), rounded up.
    /// Builds are not in the allowance: each is charged at this plus the
    /// margin.
    pub const MICROS_PER_BUILD_SECOND: i64 = 21;
    /// Custom domains across the workspace (Cloudflare for SaaS custom
    /// hostnames); each one past these is charged by the month.
    pub const CUSTOM_DOMAINS: u32 = 3;
    /// What one custom hostname costs g1t a month: $0.10.
    pub const MICROS_PER_DOMAIN_MONTH: i64 = 100_000;
}

/// `record_sandbox`: how long one sandbox ran for a workspace, reported by
/// the runner when it stops. Every sandbox g1t starts for a workspace
/// (agents, reviews, checks, the merge queue, workflow jobs) is metered by
/// the second, from the first: recorded once per `reference`, with what it
/// cost g1t, and charged at the price book's `sandbox_second` price unless
/// `FREE_WHILE_BUILDING`. Deploy builds are charged by the Deployments plan
/// instead.
/// Returns `Outcome<bool>`: false if that reference was recorded before.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RecordSandboxArgs {
    pub workspace: String,
    pub seconds: u32,
    /// What ran, e.g. `Checks on acme/api#12`.
    pub description: String,
    /// `namespace/name`.
    pub repo: Option<String>,
    /// Unique to the run.
    pub reference: String,
}

/// How much a workspace has earned g1t's trust with money, which sets how
/// far its unpaid usage can go before its work stops.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Trust {
    /// No live payment yet: only a little past the free allowances.
    New,
    /// Has paid g1t real money: the ceiling grows with what it has paid.
    Paid,
    /// Has paid steadily for months, with nothing disputed or declined:
    /// the ceiling follows its monthly spend, up to $10,000, by itself.
    Established,
    /// A ceiling g1t set by hand, after talking to the workspace.
    Reviewed,
    /// g1t's own workspaces: no ceiling.
    Internal,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LimitState {
    Ok,
    /// Past 80% of the ceiling.
    Warning,
    /// At or past it: no new sandboxes, builds or app requests.
    Stopped,
}

/// How far a workspace's unpaid usage has gone this month, and where its
/// work stops: like Fly's or Cloudflare's limits for new accounts, so no
/// one runs up costs g1t cannot collect. Usage counts at what it cost g1t
/// or what it is charged, whichever is more, so it counts while g1t is
/// free too.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Limit {
    pub workspace: String,
    /// The account that pays, whose usage and payments the limit counts:
    /// the workspace's own, or its enterprise's.
    #[serde(default)]
    pub account: String,
    #[serde(default)]
    pub account_name: String,
    pub trust: Trust,
    /// Usage this month (UTC) less what was paid this month.
    pub exposure_micros: i64,
    /// Where work stops: the lower of g1t's ceiling and the owner's own
    /// spend limit. None for g1t's own workspaces.
    pub ceiling_micros: Option<i64>,
    /// The ceiling g1t sets from `trust`.
    pub trust_ceiling_micros: Option<i64>,
    /// The owner's own monthly limit, if they set one.
    pub spend_limit_micros: Option<i64>,
    pub state: LimitState,
    /// What to tell people when work is stopped or close to it.
    pub message: Option<String>,
    /// Charged this month, which the spend limit is measured against.
    #[serde(default)]
    pub spent_micros: i64,
    /// True while the owners have not chosen a spend limit of their own, so
    /// the automatic one applies: $200, or twice last month's spend.
    #[serde(default)]
    pub default_spend_limit: bool,
    /// The most the owners may set their own limit to: g1t's ceiling. To
    /// go past it, they contact g1t.
    #[serde(default)]
    pub available_micros: Option<i64>,
    /// How the ceiling grows from here, in a sentence.
    #[serde(default)]
    pub growth: Option<String>,
}

/// `limit`: a workspace's limit, for its members. Returns `Outcome<Limit>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct LimitArgs {
    pub workspace: String,
    pub viewer: Viewer,
}

/// `check_limit`: the same, for the services that enforce it. Returns
/// `Outcome<Limit>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct CheckLimitArgs {
    pub workspace: String,
}

/// `note_pending`: usage this month that will be charged later, such as
/// app traffic past a plan, so the workspace's limit counts it now. Each
/// report replaces the last for that workspace, source and month. Called
/// by the service that meters it. Returns `bool`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NotePendingArgs {
    pub workspace: String,
    /// `deployments`.
    pub source: String,
    /// What it cost g1t so far this month, before the margin.
    pub cost_micros: i64,
}

/// `set_spend_limit`: the owner's own monthly ceiling, under g1t's; None
/// removes it. Owners only. Returns `Outcome<Limit>`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SetSpendLimitArgs {
    pub actor: User,
    pub workspace: String,
    /// A monthly limit, at most what is available; None goes back to the
    /// default.
    pub spend_limit_micros: Option<i64>,
    /// Use everything available, with no limit of their own.
    #[serde(default)]
    pub use_full_limit: bool,
}

/// One metered unit: what it costs g1t, and what it is sold at. The price
/// is always `cost × (100 + markup) / 100`, so it follows the cost.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Price {
    /// `sandbox_second`, `build_second`, `app_requests`, `app_cpu`, `app_month`.
    pub meter: String,
    pub title: String,
    pub unit: String,
    /// Millionths of a dollar per unit; may have a fraction.
    pub cost_micros: f64,
    pub markup_percent: u32,
    pub price_micros: f64,
    /// `list`: Cloudflare's published price. `cloudflare`: what Cloudflare
    /// actually billed g1t, measured.
    pub source: String,
    /// When it was last checked against Cloudflare's bill.
    pub checked_at: Option<String>,
    pub updated_at: String,
}

impl Price {
    pub fn price_for(cost_micros: f64, markup_percent: u32) -> f64 {
        cost_micros * f64::from(100 + markup_percent) / 100.0
    }
}

/// A cost that moved.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PriceChange {
    pub meter: String,
    pub old_cost_micros: f64,
    pub new_cost_micros: f64,
    pub markup_percent: u32,
    /// The markup before, when the change was to the markup rather than
    /// to the cost. Absent when the markup stayed `markup_percent`.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub old_markup_percent: Option<u32>,
    pub reason: String,
    pub created_at: String,
}

/// `prices`: every metered price and the recent changes. Public. Returns
/// `PriceBook`.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PriceBook {
    pub prices: Vec<Price>,
    pub changes: Vec<PriceChange>,
    /// The margin on model usage, which is charged at what AI Gateway
    /// priced each request at.
    pub model_margin_percent: u32,
}

/// Who pays: a billing account. Every workspace has one; by default its
/// own. An enterprise account pays for several workspaces at once, as
/// GitHub Enterprise does: one bill, one limit, one set of terms.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BillingAccount {
    /// `ws_<slug>` for a workspace's own account; `ent_…` for an enterprise.
    pub id: String,
    pub kind: AccountKind,
    pub name: String,
    pub terms: Terms,
    /// The workspaces it pays for.
    pub workspaces: Vec<String>,
    /// Where an enterprise's invoices go.
    #[serde(default)]
    pub billing_email: Option<String>,
    /// An enterprise's invoices, newest first. Empty for a workspace's own.
    #[serde(default)]
    pub invoices: Vec<EnterpriseInvoice>,
    pub created_at: String,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AccountKind {
    Workspace,
    Enterprise,
}

/// How an account is charged. Standard unless g1t set otherwise in sudo.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Terms {
    pub kind: TermsKind,
    /// Off every usage charge, in percent. Custom terms only.
    #[serde(default)]
    pub discount_percent: u32,
    /// A ceiling on unpaid usage that replaces the one trust would give.
    #[serde(default)]
    pub ceiling_micros: Option<i64>,
    /// Why, for whoever looks next.
    #[serde(default)]
    pub note: String,
    /// When the terms end and the account goes back to standard.
    #[serde(default)]
    pub until: Option<String>,
    #[serde(default)]
    pub set_by: Option<String>,
    #[serde(default)]
    pub set_at: Option<String>,
}

impl Terms {
    pub fn standard() -> Self {
        Terms {
            kind: TermsKind::Standard,
            discount_percent: 0,
            ceiling_micros: None,
            note: String::new(),
            until: None,
            set_by: None,
            set_at: None,
        }
    }

    /// What a charge becomes under these terms.
    pub fn apply(&self, charge_micros: i64) -> i64 {
        match self.kind {
            TermsKind::Comped => 0,
            TermsKind::Custom => charge_micros * i64::from(100 - self.discount_percent.min(100)) / 100,
            TermsKind::Standard => charge_micros,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TermsKind {
    /// Prices as published, limits by trust.
    Standard,
    /// Nothing charged; usage still recorded with its cost. Paid features
    /// are on without a plan. For g1t's own workspaces, partners, and the
    /// like.
    Comped,
    /// A discount, a ceiling, or both.
    Custom,
}

/// `stripe_webhook`: an event from Stripe, as the API received it: the raw
/// body and its `Stripe-Signature` header. Billing checks the signature
/// against the secret of the endpoint it registered, and handles each
/// event once. Returns `Outcome<bool>`: false for one already handled.
#[derive(Debug, Serialize, Deserialize)]
pub struct StripeWebhookArgs {
    pub payload: String,
    pub signature: String,
}

/// `admin_stripe`: where billing stands with Stripe. Staff only. Returns
/// `StripeStatus`. With `setup: true`, registers (or replaces) the webhook
/// endpoint for the current mode first.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AdminStripeArgs {
    #[serde(default)]
    pub setup: bool,
    #[serde(default)]
    pub by: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeStatus {
    /// `test` or `live`, from the key; `off` without one.
    pub mode: String,
    pub webhook: Option<StripeWebhook>,
    /// The latest events handled, newest first.
    pub recent_events: Vec<StripeEventSummary>,
    /// What went wrong setting up, if it did.
    pub error: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeWebhook {
    pub url: String,
    pub endpoint_id: String,
    pub events: Vec<String>,
    pub created_by: String,
    pub created_at: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StripeEventSummary {
    pub id: String,
    pub kind: String,
    pub outcome: String,
    pub received_at: String,
}

/// `admin_enterprise_billing`: where an enterprise's invoices go. Creates
/// or updates its Stripe customer. Returns `Outcome<BillingAccount>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminEnterpriseBillingArgs {
    pub id: String,
    pub email: String,
    pub by: String,
}

/// `admin_invoice_enterprise`: sends an enterprise its invoice now, for
/// what its workspaces owe, rather than waiting for the month to close.
/// Returns `Outcome<EnterpriseInvoice>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminInvoiceEnterpriseArgs {
    pub id: String,
    pub by: String,
}

/// An enterprise's invoice: one line per workspace, paid on Stripe.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EnterpriseInvoice {
    pub invoice_id: String,
    /// Stripe's page for it, where it is paid.
    pub hosted_url: Option<String>,
    pub amount_micros: i64,
    /// `open`, `paid`, `overdue` or `void`.
    pub status: String,
    pub period: String,
    pub lines: Vec<InvoiceLine>,
    pub created_at: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InvoiceLine {
    pub workspace: String,
    pub amount_micros: i64,
}

/// A workspace's invoice from g1t: one per month, and one each time it is
/// charged near its limit. Itemised, charged to the card on file, and kept
/// in Stripe's billing page with its PDF.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceInvoice {
    pub invoice_id: String,
    pub workspace: String,
    /// `month` (2026-10) or `threshold`.
    pub reason: String,
    pub period: String,
    pub amount_micros: i64,
    /// `paid`, `open`, `failed` or `void`.
    pub status: String,
    pub hosted_url: Option<String>,
    pub pdf_url: Option<String>,
    pub lines: Vec<InvoiceItem>,
    pub created_at: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InvoiceItem {
    pub description: String,
    pub amount_micros: i64,
}

/// `invoices`: a workspace's invoices from g1t, newest first. Members
/// only. Returns `Outcome<Vec<WorkspaceInvoice>>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct InvoicesArgs {
    pub workspace: String,
    pub viewer: Viewer,
}

/// `admin_workspace_invoices`: the same, for staff. Returns
/// `Vec<WorkspaceInvoice>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminWorkspaceInvoicesArgs {
    pub workspace: String,
}

/// `statement`: a month of a workspace's ledger, grouped by day (or by
/// project) with a line per kind of charge. Members only. Returns
/// `Outcome<Statement>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct StatementArgs {
    pub workspace: String,
    pub viewer: Viewer,
    /// YYYY-MM; this month when absent.
    #[serde(default)]
    pub month: Option<String>,
    /// `day` (the default) or `project`.
    #[serde(default)]
    pub group: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Statement {
    pub month: String,
    /// Months with any entries, newest first.
    pub months: Vec<String>,
    pub groups: Vec<StatementGroup>,
    pub totals: StatementTotals,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StatementGroup {
    /// The day (YYYY-MM-DD) or the project (`owner/name`, or empty).
    pub key: String,
    pub label: String,
    pub lines: Vec<StatementLine>,
    /// What the group's charges come to.
    pub charged_micros: i64,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StatementLine {
    /// Agent runs, Sandbox time, Deployments, Payments, Credits from g1t,
    /// Refunds, and, for older entries, Runs on your own model provider.
    pub kind: String,
    pub count: u32,
    /// Charges positive; money in (payments, credits) negative.
    pub charged_micros: i64,
    pub cost_micros: i64,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StatementTotals {
    pub charged_micros: i64,
    pub paid_micros: i64,
    pub cost_micros: i64,
    pub entries: u32,
}

/// `statement_entries`: one statement line's entries, newest first, 50 at
/// a time (`before` = the last id seen). Returns `Outcome<Vec<LedgerEntry>>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct StatementEntriesArgs {
    pub workspace: String,
    pub viewer: Viewer,
    pub month: String,
    pub kind: String,
    #[serde(default)]
    pub day: Option<String>,
    #[serde(default)]
    pub project: Option<String>,
    #[serde(default)]
    pub before: Option<String>,
}

// --- Sales (sudo.g1t.sh) ------------------------------------------------------
//
// What staff need to know to reach out: who is growing, who is close to
// their limit, who was declined, who has become a steady customer. And what
// was done about it: a stage, an owner on g1t's side, a next step, notes.

/// Why a workspace is worth a look.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SignalKind {
    /// At its limit, or its own spend limit: work is stopped.
    AtLimit,
    /// Past 80% of what is available to it: about to need more.
    NearCeiling,
    /// Its card was declined or a payment disputed.
    Declined,
    /// This month is well ahead of last month.
    Growing,
    /// Became Established: the ceiling now follows its spend.
    Established,
    /// Paid g1t for the first time.
    FirstPayment,
    /// Spending enough that custom terms or an enterprise may suit it.
    HighSpend,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Signal {
    pub workspace: String,
    pub kind: SignalKind,
    /// One sentence, with the figures.
    pub detail: String,
    /// The figure that matters, such as this month's spend.
    pub value_micros: i64,
    /// Its sales stage, if staff gave it one.
    pub stage: Option<String>,
    pub owner: Option<String>,
    #[serde(default)]
    pub next_step: Option<String>,
    /// When the next step is due, `YYYY-MM-DD`.
    #[serde(default)]
    pub next_at: Option<String>,
}

/// `admin_invoices`: every invoice g1t has sent, workspaces' and
/// enterprises', newest first. Returns `Vec<InvoiceSummary>`.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AdminInvoicesArgs {
    /// `paid`, `open`, `failed`, `overdue` or `void`.
    #[serde(default)]
    pub status: Option<String>,
    /// YYYY-MM, by when it was sent.
    #[serde(default)]
    pub month: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InvoiceSummary {
    pub invoice_id: String,
    /// `workspace` or `enterprise`.
    pub kind: String,
    /// The workspace's slug, or the enterprise's account id.
    pub account: String,
    /// What to call it: the workspace, or the enterprise's name.
    pub name: String,
    pub reason: String,
    pub period: String,
    pub amount_micros: i64,
    pub status: String,
    pub hosted_url: Option<String>,
    pub created_at: String,
    pub paid_at: Option<String>,
}

/// `admin_audit`: every change made in sudo, and by Stripe, newest first.
/// Returns `Vec<AdminAction>`.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AdminAuditArgs {
    #[serde(default)]
    pub by: Option<String>,
    #[serde(default)]
    pub action: Option<String>,
    /// Only those before this time, for paging.
    #[serde(default)]
    pub before: Option<String>,
}

/// `admin_signals`: every workspace worth reaching out to, most urgent
/// first. Returns `Vec<Signal>`.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AdminSignalsArgs {}

/// What staff are doing about a workspace.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SalesRecord {
    pub workspace: String,
    /// `none`, `lead`, `contacted`, `negotiating`, `won`, `lost` or `churn_risk`.
    pub stage: String,
    /// The staff member looking after it.
    pub owner: Option<String>,
    pub next_step: Option<String>,
    /// RFC 3339 date.
    pub next_at: Option<String>,
    pub notes: Vec<SalesNote>,
    pub updated_at: Option<String>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SalesNote {
    pub id: String,
    pub text: String,
    pub by: String,
    pub created_at: String,
}

/// `admin_sales`: a workspace's sales record. Returns `SalesRecord`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminSalesArgs {
    pub workspace: String,
}

/// `admin_set_sales`: its stage, owner and next step. Returns `Outcome<SalesRecord>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminSetSalesArgs {
    pub workspace: String,
    pub stage: String,
    #[serde(default)]
    pub owner: Option<String>,
    #[serde(default)]
    pub next_step: Option<String>,
    #[serde(default)]
    pub next_at: Option<String>,
    pub by: String,
}

/// `admin_add_note`. Returns `Outcome<SalesRecord>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminAddNoteArgs {
    pub workspace: String,
    pub text: String,
    pub by: String,
}

/// `admin_overview`: the business at a glance. Returns `Overview`.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AdminOverviewArgs {}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Overview {
    /// YYYY-MM.
    pub month: String,
    /// The last six months, oldest first, all workspaces together.
    pub months: Vec<MonthFigures>,
    /// This month by kind of usage: models, sandbox, deployments, plans.
    pub by_kind: Vec<KindFigures>,
    pub paying_workspaces: u32,
    pub stopped: u32,
    pub near_ceiling: u32,
    pub declined: u32,
    /// Sent and not yet paid, workspaces and enterprises.
    pub open_invoices_micros: i64,
    /// Follow-ups due today or earlier.
    pub follow_ups_due: u32,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KindFigures {
    pub kind: String,
    pub charged_micros: i64,
    pub cost_micros: i64,
}

// --- Staff (sudo.g1t.sh) ------------------------------------------------------
//
// Called only by the sudo app, which only g1t staff can reach (behind
// Cloudflare Access). Each change names who made it, and is kept in the
// audit log.

/// `admin_accounts`: every billing account, with where each stands this
/// month. Returns `Vec<AccountSummary>`.
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AdminAccountsArgs {
    #[serde(default)]
    pub query: Option<String>,
    /// Exactly these workspaces' accounts, such as one page of sudo's
    /// list; every account with activity when absent.
    #[serde(default)]
    pub workspaces: Option<Vec<String>>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountSummary {
    pub account: BillingAccount,
    pub limit: Limit,
    /// Charged this month, after terms.
    pub charged_micros: i64,
    /// What this month's usage cost g1t.
    pub cost_micros: i64,
    /// Paid, ever.
    pub paid_micros: i64,
    /// The same figures for each of the account's workspaces that has
    /// any, so staff can see what one member of an enterprise used.
    #[serde(default)]
    pub by_workspace: Vec<WorkspaceFigures>,
    /// The last six months, oldest first, for trends.
    #[serde(default)]
    pub months: Vec<MonthFigures>,
}

/// One month of an account's billing.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MonthFigures {
    /// YYYY-MM.
    pub month: String,
    pub charged_micros: i64,
    pub cost_micros: i64,
    pub paid_micros: i64,
}

/// One workspace's share of an [`AccountSummary`].
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorkspaceFigures {
    pub workspace: String,
    pub charged_micros: i64,
    pub cost_micros: i64,
    pub paid_micros: i64,
}

/// `admin_account`: one account in full. Returns `Outcome<AccountDetail>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminAccountArgs {
    /// An account id, or a workspace slug.
    pub id: String,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountDetail {
    pub summary: AccountSummary,
    /// Each workspace's limit, for an enterprise.
    pub workspaces: Vec<Limit>,
    pub ledger: Vec<LedgerEntry>,
    pub audit: Vec<AdminAction>,
}

/// `admin_set_terms`. Returns `Outcome<BillingAccount>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminSetTermsArgs {
    pub id: String,
    pub terms: Terms,
    pub by: String,
}

/// `admin_create_enterprise`. Returns `Outcome<BillingAccount>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminCreateEnterpriseArgs {
    pub name: String,
    pub workspaces: Vec<String>,
    pub by: String,
}

/// `admin_attach`: moves a workspace onto an enterprise account, or back
/// onto its own with `account: None`. Returns `Outcome<BillingAccount>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminAttachArgs {
    pub workspace: String,
    pub account: Option<String>,
    pub by: String,
}

/// `admin_credit`: money g1t gives a workspace, such as a refund or a
/// goodwill credit. Returns `Outcome<LedgerEntry>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct AdminCreditArgs {
    pub workspace: String,
    pub amount_micros: i64,
    pub note: String,
    pub by: String,
}

/// One change made in sudo.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdminAction {
    pub id: String,
    pub account: String,
    pub action: String,
    pub detail: String,
    pub by: String,
    pub created_at: String,
}

/// What a feature's plan costs and includes.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Plan {
    pub feature: Feature,
    pub title: String,
    /// Charged every month while the plan is on, in cents.
    pub monthly_cents: u32,
    /// What the monthly price includes, one line each, for people to read.
    pub includes: Vec<String>,
    /// How usage past the allowance is charged, for people to read.
    pub overage: String,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SubscriptionStatus {
    /// Paid up; the feature works.
    Active,
    /// Paid up to the end of the period, and ends then.
    Canceling,
    /// The last payment failed; the feature is off until it is paid.
    PastDue,
    /// Ended.
    Canceled,
}

impl SubscriptionStatus {
    /// Whether the feature works in this state.
    pub fn on(self) -> bool {
        matches!(self, SubscriptionStatus::Active | SubscriptionStatus::Canceling)
    }
}

/// A workspace's plan for one feature.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Subscription {
    pub feature: Feature,
    pub status: SubscriptionStatus,
    /// RFC 3339: when the period paid for ends, and the plan renews or
    /// ends.
    pub period_end: Option<String>,
    /// Username of whoever turned it on.
    pub started_by: String,
    /// RFC 3339.
    pub started_at: String,
}

/// A feature as a workspace sees it: what it costs, and its plan if it has
/// one.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FeatureState {
    pub plan: Plan,
    pub subscription: Option<Subscription>,
    /// Whether the feature works for the workspace now.
    pub on: bool,
}

/// `features`: every paid feature and the workspace's plan for each.
/// Members only. Returns `Outcome<Vec<FeatureState>>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct FeaturesArgs {
    pub workspace: String,
    pub viewer: Viewer,
}

/// `subscribe`: starts the card page for a feature's monthly plan. Owners
/// only. Returns `Outcome<Checkout>`; the page's id comes back to
/// `return_url` as `session`, for `confirm_subscription`.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SubscribeArgs {
    pub actor: User,
    pub workspace: String,
    pub feature: Feature,
    pub return_url: String,
}

/// `confirm_subscription`: turns the feature on once the processor says
/// the plan was paid for. Safe to call any number of times. Returns
/// `Outcome<FeatureState>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfirmSubscriptionArgs {
    pub workspace: String,
    pub viewer: Viewer,
    pub session: String,
}

/// `cancel_subscription` (`resume` false) ends a plan at the end of the
/// period paid for; with `resume` true, takes that back. Owners only.
/// Returns `Outcome<FeatureState>`.
#[derive(Debug, Serialize, Deserialize)]
pub struct CancelSubscriptionArgs {
    pub actor: User,
    pub workspace: String,
    pub feature: Feature,
    #[serde(default)]
    pub resume: bool,
}

/// `has_feature`: whether a feature works for a workspace now, asked by the
/// service that provides it before doing paid work. Returns
/// `Outcome<bool>`: a failure, with the reason to show, when it does not.
/// True everywhere when no card processor is configured.
#[derive(Debug, Serialize, Deserialize)]
pub struct HasFeatureArgs {
    pub workspace: String,
    pub feature: Feature,
}

/// `charge_feature`: usage of a feature past its plan's allowance, charged
/// from the workspace's credit at cost plus the margin, whatever
/// `FREE_WHILE_BUILDING` says. Called by the service that provides it.
/// Charged once per `reference`. Returns `Outcome<bool>`: false if that
/// reference was charged before.
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChargeFeatureArgs {
    pub workspace: String,
    pub feature: Feature,
    /// What it cost g1t, in millionths of a dollar, before the margin.
    pub cost_micros: i64,
    pub description: String,
    /// `namespace/name`, when the usage was one repository's.
    pub repo: Option<String>,
    /// Unique to this charge, e.g. `deployments/acme/2026-10`.
    pub reference: String,
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn an_account_carries_no_run_fee() {
        let account = Account {
            workspace: "acme".into(),
            balance_micros: 0,
            status: Status { enabled: true, live: false, free: false },
            margin_percent: 20,
            card: None,
        };
        let json = serde_json::to_value(account).unwrap();
        let mut keys: Vec<&str> = json.as_object().unwrap().keys().map(String::as_str).collect();
        keys.sort_unstable();
        assert_eq!(keys, ["balanceMicros", "card", "marginPercent", "status", "workspace"]);
    }

    #[test]
    fn a_price_change_says_when_the_markup_moved() {
        let change = PriceChange {
            meter: "sandbox_second".into(),
            old_cost_micros: 21.0,
            new_cost_micros: 21.0,
            markup_percent: 20,
            old_markup_percent: Some(138),
            reason: "Sandbox time is now charged at cost plus 20% from the first second".into(),
            created_at: "2026-10-05T00:00:00Z".into(),
        };
        assert_eq!(serde_json::to_value(&change).unwrap()["oldMarkupPercent"], 138);
        let cost_only = PriceChange { old_markup_percent: None, ..change };
        assert!(serde_json::to_value(&cost_only).unwrap().get("oldMarkupPercent").is_none());
    }

    #[test]
    fn features_are_named_as_the_site_sends_them() {
        assert_eq!(
            serde_json::to_value(Feature::Deployments).unwrap(),
            serde_json::json!("deployments")
        );
        assert_eq!(Feature::parse("deployments"), Some(Feature::Deployments));
        assert!(SubscriptionStatus::Canceling.on());
        assert!(!SubscriptionStatus::PastDue.on());
    }

    #[test]
    fn who_pays_is_read_as_the_runner_sends_it() {
        let run: StartRunArgs = serde_json::from_value(serde_json::json!({
            "workspace": "acme",
            "repo": { "namespace": "acme", "name": "web" },
            "number": 7,
            "task": "implement",
            "model": "Claude Sonnet 5.5",
            "billedTo": "workspace",
        }))
        .unwrap();
        assert_eq!(run.billed_to, "workspace");
    }
}