92 lines · 3.8 KB
import { all } from "../db";
import { accountDb, stmt } from "../scoped-db";
import { invalidateUnread } from "../user-inbox";
import { randomId } from "../crypto";
import { publicUrl } from "../events/emit";
import { meterDef } from "~/lib/meters";
import { crossedThreshold } from "~/lib/plans";
import { periodOf } from "./record";
import { usageSummary } from "./summary";

type AlertCandidate = { subject: string; threshold: number; title: string };

/** Pure: which thresholds a summary has crossed (per included meter, plus the spending limit). */
export function alertCandidates(summary: Awaited<ReturnType<typeof usageSummary>>): AlertCandidate[] {
  const out: AlertCandidate[] = [];
  for (const meter of summary.meters) {
    const limit = meter.hardCap ?? meter.included;
    if (limit === null || limit <= 0) continue;
    const threshold = crossedThreshold(meter.quantity, limit);
    if (threshold === null) continue;
    const label = meterDef(meter.meter)?.label ?? meter.meter;
    out.push({ subject: meter.meter, threshold, title: `${label}: ${threshold}% of this month's allowance used` });
  }
  if (summary.spendingLimitCents !== null && summary.spendingLimitCents > 0) {
    const threshold = crossedThreshold(summary.totalCents, summary.spendingLimitCents);
    if (threshold !== null) {
      out.push({ subject: "spend", threshold, title: `Spending: ${threshold}% of your spending limit used` });
    }
  }
  return out;
}

async function recipients(accountId: string): Promise<{ ids: string[]; url: string }> {
  const account = await all<{ kind: string; name: string }>("SELECT kind, name FROM accounts WHERE id = ?", accountId);
  if (account[0]?.kind === "org") {
    const owners = await all<{ user_id: string }>(
      "SELECT user_id FROM org_members WHERE org_id = ? AND role = 'owner' LIMIT 50",
      accountId,
    );
    return { ids: owners.map((row) => row.user_id), url: publicUrl(`/orgs/${account[0].name}/billing`) };