flagon-io/g1t

public

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

Commit

Billing lists every invoice and every staff change; signals carry follow-ups

admin_invoices: workspace and enterprise invoices together, by status and month. admin_audit: every change made in sudo and by Stripe, by who and what, paged. Signals now include the next step and its date, so sudo's Reach out can show follow-ups due.

syntaqxcommitted Parent2c8fbdcBrowse files
5 files+174−10/5 viewed
+49−0
819819 /// Its sales stage, if staff gave it one.
820820 pub stage: Option<String>,
821821 pub owner: Option<String>,
822+ #[serde(default)]
823+ pub next_step: Option<String>,
824+ /// When the next step is due, `YYYY-MM-DD`.
825+ #[serde(default)]
826+ pub next_at: Option<String>,
827+}
828+
829+/// `admin_invoices`: every invoice g1t has sent, workspaces' and
830+/// enterprises', newest first. Returns `Vec<InvoiceSummary>`.
831+#[derive(Debug, Default, Serialize, Deserialize)]
832+pub struct AdminInvoicesArgs {
833+ /// `paid`, `open`, `failed`, `overdue` or `void`.
834+ #[serde(default)]
835+ pub status: Option<String>,
836+ /// YYYY-MM, by when it was sent.
837+ #[serde(default)]
838+ pub month: Option<String>,
839+}
840+
841+#[derive(Clone, Debug, Serialize, Deserialize)]
842+#[serde(rename_all = "camelCase")]
843+pub struct InvoiceSummary {
844+ pub invoice_id: String,
845+ /// `workspace` or `enterprise`.
846+ pub kind: String,
847+ /// The workspace's slug, or the enterprise's account id.
848+ pub account: String,
849+ /// What to call it: the workspace, or the enterprise's name.
850+ pub name: String,
851+ pub reason: String,
852+ pub period: String,
853+ pub amount_micros: i64,
854+ pub status: String,
855+ pub hosted_url: Option<String>,
856+ pub created_at: String,
857+ pub paid_at: Option<String>,
858+}
859+
860+/// `admin_audit`: every change made in sudo, and by Stripe, newest first.
861+/// Returns `Vec<AdminAction>`.
862+#[derive(Debug, Default, Serialize, Deserialize)]
863+pub struct AdminAuditArgs {
864+ #[serde(default)]
865+ pub by: Option<String>,
866+ #[serde(default)]
867+ pub action: Option<String>,
868+ /// Only those before this time, for paging.
869+ #[serde(default)]
870+ pub before: Option<String>,
822871 }
823872
824873 /// `admin_signals`: every workspace worth reaching out to, most urgent
+22−0
164164 valueMicros: number;
165165 stage: string | null;
166166 owner: string | null;
167+ nextStep?: string | null;
168+ /** When the next step is due, YYYY-MM-DD. */
169+ nextAt?: string | null;
170+};
171+
172+/** One invoice g1t has sent, a workspace's or an enterprise's. */
173+export type InvoiceSummary = {
174+ invoiceId: string;
175+ kind: "workspace" | "enterprise";
176+ account: string;
177+ name: string;
178+ reason: string;
179+ period: string;
180+ amountMicros: number;
181+ status: string;
182+ hostedUrl: string | null;
183+ createdAt: string;
184+ paidAt: string | null;
167185 };
168186
169187 export type SalesStage = "none" | "lead" | "contacted" | "negotiating" | "won" | "lost" | "churn_risk";
237255 addNote(workspace: string, text: string, by: string): Promise<Result<SalesRecord>>;
238256 /** A workspace's invoices from g1t, for staff. */
239257 workspaceInvoices(workspace: string): Promise<WorkspaceInvoice[]>;
258+ /** Every invoice g1t has sent, newest first. */
259+ allInvoices(filter?: { status?: string; month?: string }): Promise<InvoiceSummary[]>;
260+ /** Every change made in sudo and by Stripe, newest first, 100 at a time. */
261+ audit(filter?: { by?: string; action?: string; before?: string }): Promise<AdminAction[]>;
240262 }
241263
242264 /** How much a workspace has earned g1t's trust with money. */
+3−0
248248 }),
249249 addNote: (workspace, text, by) => call("admin_add_note", { workspace, text, by }),
250250 workspaceInvoices: (workspace) => call("admin_workspace_invoices", { workspace }),
251+ allInvoices: (filter = {}) => call("admin_invoices", { status: filter.status ?? null, month: filter.month ?? null }),
252+ audit: (filter = {}) =>
253+ call("admin_audit", { by: filter.by ?? null, action: filter.action ?? null, before: filter.before ?? null }),
251254 };
252255 }
253256
+2−0
10281028 "admin_sales" => reply(&billing.admin_sales(args(body)?).await?),
10291029 "admin_set_sales" => reply(&billing.admin_set_sales(args(body)?).await?),
10301030 "admin_add_note" => reply(&billing.admin_add_note(args(body)?).await?),
1031+ "admin_invoices" => reply(&billing.admin_invoices(args(body)?).await?),
1032+ "admin_audit" => reply(&billing.admin_audit(args(body)?).await?),
10311033 "note_pending" => reply(&billing.note_pending(args(body)?).await?),
10321034 "admin_accounts" => reply(&billing.admin_accounts(args(body)?).await?),
10331035 "admin_account" => reply(&billing.admin_account(args(body)?).await?),
+98−1
272272 let this_month = row.this_month.unwrap_or(0).max(0);
273273 let last_month = row.last_month.unwrap_or(0).max(0);
274274 let record = self.record_row(&row.workspace).await?;
275− let (stage, owner) = record.map_or((None, None), |r| (Some(r.stage), r.owner));
275+ let (stage, owner, next_step, next_at) =
276+ record.map_or((None, None, None, None), |r| (Some(r.stage), r.owner, r.next_step, r.next_at));
276277 let mut push = |kind: SignalKind, detail: String, value: i64| {
277278 signals.push(Signal {
278279 workspace: row.workspace.clone(),
281282 value_micros: value,
282283 stage: stage.clone(),
283284 owner: owner.clone(),
285+ next_step: next_step.clone(),
286+ next_at: next_at.clone(),
284287 });
285288 };
286289 let declined = limit.message.as_deref().is_some_and(|m| m.contains("could not be charged"));
342345 Ok(signals)
343346 }
344347
348+ /// Every invoice g1t has sent, workspaces' and enterprises'.
349+ pub(crate) async fn admin_invoices(&self, a: g1t_contracts::billing::AdminInvoicesArgs) -> Result<Vec<g1t_contracts::billing::InvoiceSummary>> {
350+ #[derive(Deserialize)]
351+ struct Row {
352+ invoice_id: String,
353+ kind: String,
354+ account: String,
355+ name: String,
356+ reason: String,
357+ period: String,
358+ amount_micros: i64,
359+ status: String,
360+ hosted_url: Option<String>,
361+ created_at: String,
362+ paid_at: Option<String>,
363+ }
364+ let status = a.status.filter(|s| ["paid", "open", "failed", "overdue", "void"].contains(&s.as_str()));
365+ let month = a.month.filter(|m| m.len() == 7 && m.chars().all(|c| c.is_ascii_digit() || c == '-'));
366+ let rows = self
367+ .db
368+ .prepare(
369+ "SELECT * FROM (
370+ SELECT invoice_id, 'workspace' AS kind, workspace AS account, workspace AS name, reason, period,
371+ amount_micros, status, hosted_url, created_at, paid_at
372+ FROM workspace_invoices
373+ UNION ALL
374+ SELECT i.invoice_id, 'enterprise', i.account_id, COALESCE(b.name, i.account_id), 'enterprise', i.period,
375+ i.amount_micros, i.status, i.hosted_url, i.created_at, i.paid_at
376+ FROM enterprise_invoices i LEFT JOIN billing_accounts b ON b.id = i.account_id
377+ )
378+ WHERE (?1 IS NULL OR status = ?1) AND (?2 IS NULL OR substr(created_at, 1, 7) = ?2)
379+ ORDER BY created_at DESC LIMIT 200",
380+ )
381+ .bind(&[crate::optional(status.as_deref()), crate::optional(month.as_deref())])?
382+ .all()
383+ .await?
384+ .results::<Row>()?;
385+ Ok(rows
386+ .into_iter()
387+ .map(|r| g1t_contracts::billing::InvoiceSummary {
388+ invoice_id: r.invoice_id,
389+ kind: r.kind,
390+ account: r.account,
391+ name: r.name,
392+ reason: r.reason,
393+ period: r.period,
394+ amount_micros: r.amount_micros,
395+ status: r.status,
396+ hosted_url: r.hosted_url,
397+ created_at: r.created_at,
398+ paid_at: r.paid_at,
399+ })
400+ .collect())
401+ }
402+
403+ /// Every change made in sudo and by Stripe, newest first.
404+ pub(crate) async fn admin_audit(&self, a: g1t_contracts::billing::AdminAuditArgs) -> Result<Vec<g1t_contracts::billing::AdminAction>> {
405+ #[derive(Deserialize)]
406+ struct Row {
407+ id: String,
408+ account: String,
409+ action: String,
410+ detail: String,
411+ by: String,
412+ created_at: String,
413+ }
414+ let rows = self
415+ .db
416+ .prepare(
417+ "SELECT id, account, action, detail, by, created_at FROM admin_actions
418+ WHERE (?1 IS NULL OR by = ?1) AND (?2 IS NULL OR action = ?2) AND (?3 IS NULL OR created_at < ?3)
419+ ORDER BY created_at DESC LIMIT 100",
420+ )
421+ .bind(&[
422+ crate::optional(a.by.as_deref().map(str::trim).filter(|s| !s.is_empty())),
423+ crate::optional(a.action.as_deref().map(str::trim).filter(|s| !s.is_empty())),
424+ crate::optional(a.before.as_deref()),
425+ ])?
426+ .all()
427+ .await?
428+ .results::<Row>()?;
429+ Ok(rows
430+ .into_iter()
431+ .map(|r| g1t_contracts::billing::AdminAction {
432+ id: r.id,
433+ account: r.account,
434+ action: r.action,
435+ detail: r.detail,
436+ by: r.by,
437+ created_at: r.created_at,
438+ })
439+ .collect())
440+ }
441+
345442 /// The business at a glance.
346443 pub(crate) async fn admin_overview(&self, _: AdminOverviewArgs) -> Result<Overview> {
347444 let now = rfc3339(now_ms());