{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "consumption",
  "title": "Consumption metrics",
  "description": "Types, unit conversion, allowances, and cost math for the Neon v2 consumption API.",
  "files": [
    {
      "path": "src/lib/consumption.ts",
      "content": "/**\n * Consumption metrics: shapes, unit conversion, and cost math for the Neon\n * v2 consumption API (`/consumption_history/v2/projects` and `/branches`).\n *\n * The API reports raw units — CU-seconds, byte-hours, branch-hours — which\n * are not what an invoice or a dashboard shows. Every conversion Neon\n * documents lives here once, so a chart, a breakdown, and a cost estimate\n * can't drift from each other or from the bill.\n *\n * See https://neon.com/docs/introduction/usage-calculations.\n */\n\n/** The eight billable metrics returned by the per-project endpoint. */\nexport const CONSUMPTION_METRICS = [\n  \"compute_unit_seconds\",\n  \"root_branch_bytes_month\",\n  \"child_branch_bytes_month\",\n  \"instant_restore_bytes_month\",\n  \"snapshot_storage_bytes_month\",\n  \"public_network_transfer_bytes\",\n  \"private_network_transfer_bytes\",\n  \"extra_branches_month\",\n] as const;\n\nexport type ConsumptionMetricName = (typeof CONSUMPTION_METRICS)[number];\n\n/** The six metrics the per-branch endpoint supports. */\nexport const BRANCH_CONSUMPTION_METRICS = [\n  \"compute_unit_seconds\",\n  \"root_branch_bytes_month\",\n  \"child_branch_bytes_month\",\n  \"instant_restore_bytes_month\",\n  \"public_network_transfer_bytes\",\n  \"private_network_transfer_bytes\",\n] as const;\n\nexport type BranchConsumptionMetricName =\n  (typeof BRANCH_CONSUMPTION_METRICS)[number];\n\nexport type ConsumptionGranularity = \"hourly\" | \"daily\" | \"monthly\";\n\nexport type ConsumptionPlan = \"launch\" | \"scale\" | \"agent\" | \"enterprise\";\n\n/**\n * Shapes mirror the SDK's response types, optional fields and all: the API\n * omits a metric whose value was zero, and omits timeframe bounds on empty\n * buckets. Accepting that here means an SDK response drops straight in.\n */\nexport interface ConsumptionMetricValue {\n  metric_name: string;\n  value: number;\n}\n\nexport interface ConsumptionTimeframe {\n  timeframe_start?: string;\n  timeframe_end?: string;\n  metrics?: ConsumptionMetricValue[];\n}\n\nexport interface ConsumptionPeriod {\n  period_id?: string;\n  period_plan?: string;\n  period_start?: string;\n  consumption?: ConsumptionTimeframe[];\n}\n\nexport interface ConsumptionProject {\n  project_id: string;\n  periods: ConsumptionPeriod[];\n}\n\nexport interface ConsumptionBranch {\n  branch_id: string;\n  project_id: string;\n  periods: ConsumptionPeriod[];\n}\n\n/** One timeframe flattened to a metric-name -> value map. */\nexport interface ConsumptionBucket {\n  start: string;\n  end: string;\n  values: Partial<Record<ConsumptionMetricName, number>>;\n}\n\n/** Metric totals over a range, in raw API units. */\nexport type ConsumptionTotals = Partial<Record<ConsumptionMetricName, number>>;\n\n/* ── Constants ───────────────────────────────────────────── */\n\n/** Neon bills a fixed 744-hour month (31 x 24), whatever the calendar says. */\nexport const BILLING_HOURS_PER_MONTH = 744;\n/** Neon counts decimal gigabytes (10^9 bytes), not gibibytes. */\nexport const BYTES_PER_GB = 1_000_000_000;\nconst SECONDS_PER_HOUR = 3600;\nconst MS_PER_HOUR = 3_600_000;\n\n/** Free public-transfer allowance per project, in GB, on paid plans. */\nexport const PUBLIC_TRANSFER_FREE_GB = 500;\n\n/** Included branches per project, by plan; the root branch is one of them. */\nexport const BRANCHES_PER_PROJECT: Record<ConsumptionPlan, number> = {\n  agent: 25,\n  enterprise: 25,\n  launch: 10,\n  scale: 25,\n};\n\n/** Human labels for the raw metric names. */\nexport const METRIC_LABELS: Record<ConsumptionMetricName, string> = {\n  child_branch_bytes_month: \"Child branch storage\",\n  compute_unit_seconds: \"Compute\",\n  extra_branches_month: \"Extra branches\",\n  instant_restore_bytes_month: \"Instant restore\",\n  private_network_transfer_bytes: \"Private transfer\",\n  public_network_transfer_bytes: \"Public transfer\",\n  root_branch_bytes_month: \"Root branch storage\",\n  snapshot_storage_bytes_month: \"Snapshots\",\n};\n\n/** The unit a metric is billed in, once converted. */\nexport const METRIC_BILLING_UNIT: Record<ConsumptionMetricName, string> = {\n  child_branch_bytes_month: \"GB-mo\",\n  compute_unit_seconds: \"CU-hr\",\n  extra_branches_month: \"branch-mo\",\n  instant_restore_bytes_month: \"GB-mo\",\n  private_network_transfer_bytes: \"GB\",\n  public_network_transfer_bytes: \"GB\",\n  root_branch_bytes_month: \"GB-mo\",\n  snapshot_storage_bytes_month: \"GB-mo\",\n};\n\n/** The four storage metrics, in the order a breakdown should stack them. */\nexport const STORAGE_METRICS = [\n  \"root_branch_bytes_month\",\n  \"child_branch_bytes_month\",\n  \"instant_restore_bytes_month\",\n  \"snapshot_storage_bytes_month\",\n] as const satisfies readonly ConsumptionMetricName[];\n\n/**\n * One color per metric, so a hue means the same thing in every component.\n * Positional palettes drift: green is the first series in one card and a\n * different series in the next, and the reader learns the wrong lesson.\n * Pass these to a chart's `series` or a breakdown's `segments`.\n *\n * Eight metrics share five chart tokens. The assignment deliberately\n * avoids giving neighbouring steps of the ramp to metrics that stack\n * next to each other: root, child, instant restore, and snapshots read\n * mid, pale, deep, brand, so every boundary in a storage stack is a\n * large jump in lightness. A chart plotting storage and transfer at once\n * should pass its own colors.\n */\nexport const METRIC_COLORS: Record<ConsumptionMetricName, string> = {\n  child_branch_bytes_month: \"var(--chart-1)\",\n  compute_unit_seconds: \"var(--chart-2)\",\n  extra_branches_month: \"var(--chart-5)\",\n  instant_restore_bytes_month: \"var(--chart-4)\",\n  private_network_transfer_bytes: \"var(--chart-4)\",\n  public_network_transfer_bytes: \"var(--chart-5)\",\n  root_branch_bytes_month: \"var(--chart-3)\",\n  snapshot_storage_bytes_month: \"var(--chart-2)\",\n};\n\n/* ── Unit conversion ─────────────────────────────────────── */\n\n/** CU-seconds -> CU-hours. */\nexport const toCuHours = (cuSeconds: number) => cuSeconds / SECONDS_PER_HOUR;\n\n/** byte-hours -> GB-months, the billing unit. */\nexport const toGbMonths = (byteHours: number) =>\n  byteHours / BILLING_HOURS_PER_MONTH / BYTES_PER_GB;\n\n/**\n * byte-hours -> average GB held over the window, which is what the Neon\n * Console shows. Use this for \"how big is my database\", not for cost.\n */\nexport const toAverageGb = (byteHours: number, hoursInPeriod: number) =>\n  hoursInPeriod > 0 ? byteHours / hoursInPeriod / BYTES_PER_GB : 0;\n\n/** bytes -> GB. */\nexport const toGigabytes = (bytes: number) => bytes / BYTES_PER_GB;\n\n/** branch-hours -> branch-months. */\nexport const toBranchMonths = (branchHours: number) =>\n  branchHours / BILLING_HOURS_PER_MONTH;\n\n/** Whole hours between two RFC 3339 timestamps; the divisor for average GB. */\nexport const hoursBetween = (from: string | Date, to: string | Date) => {\n  const start = new Date(from).getTime();\n  const end = new Date(to).getTime();\n\n  return Math.max(0, (end - start) / MS_PER_HOUR);\n};\n\n/** Raw value -> billing unit, picked by metric. */\nexport const toBillingUnit = (\n  metric: ConsumptionMetricName,\n  value: number\n): number => {\n  if (metric === \"compute_unit_seconds\") {\n    return toCuHours(value);\n  }\n  if (metric === \"extra_branches_month\") {\n    return toBranchMonths(value);\n  }\n  if (metric.endsWith(\"_network_transfer_bytes\")) {\n    return toGigabytes(value);\n  }\n\n  return toGbMonths(value);\n};\n\n/* ── Shaping the response ────────────────────────────────── */\n\n/**\n * Flattens the API's nested periods into one bucket per timeframe, ordered\n * oldest first. Metrics that were zero are omitted from the response, so a\n * missing key means zero, not missing data.\n */\nexport const flattenConsumption = (\n  periods: readonly ConsumptionPeriod[]\n): ConsumptionBucket[] =>\n  periods\n    .flatMap((period) => period.consumption ?? [])\n    .map((timeframe) => ({\n      end: timeframe.timeframe_end ?? \"\",\n      start: timeframe.timeframe_start ?? \"\",\n      values: Object.fromEntries(\n        (timeframe.metrics ?? []).map((metric) => [\n          metric.metric_name,\n          metric.value,\n        ])\n      ) as ConsumptionBucket[\"values\"],\n    }))\n    .sort((a, b) => a.start.localeCompare(b.start));\n\n/** Sums each metric across buckets, in raw API units. */\nexport const sumBuckets = (\n  buckets: readonly ConsumptionBucket[]\n): ConsumptionTotals => {\n  const totals: ConsumptionTotals = {};\n\n  for (const bucket of buckets) {\n    for (const metric of CONSUMPTION_METRICS) {\n      const value = bucket.values[metric];\n\n      if (value !== undefined) {\n        totals[metric] = (totals[metric] ?? 0) + value;\n      }\n    }\n  }\n\n  return totals;\n};\n\n/* ── Allowances ──────────────────────────────────────────── */\n\n/** Public transfer past the per-project free allowance, in GB. */\nexport const billableTransferGb = (\n  projectGb: number,\n  allowanceGb: number = PUBLIC_TRANSFER_FREE_GB\n) => Math.max(0, projectGb - allowanceGb);\n\n/**\n * `extra_branches_month` counts every child branch, not just the ones past\n * the allowance, so subtract the included branches per bucket before\n * summing. Bucket length matters: allowance is per hour.\n */\nexport const billableBranchHours = (\n  reportedBranchHours: number,\n  plan: ConsumptionPlan,\n  hoursInBucket: number\n) => {\n  const freeChildBranches = BRANCHES_PER_PROJECT[plan] - 1;\n\n  return Math.max(0, reportedBranchHours - freeChildBranches * hoursInBucket);\n};\n\n/* ── Cost ────────────────────────────────────────────────── */\n\n/** Per-plan rates in USD per billing unit. */\nexport const PLAN_RATES: Record<\n  ConsumptionPlan,\n  Record<ConsumptionMetricName, number>\n> = {\n  agent: {\n    child_branch_bytes_month: 0.35,\n    compute_unit_seconds: 0.106,\n    extra_branches_month: 1.5,\n    instant_restore_bytes_month: 0.2,\n    private_network_transfer_bytes: 0.01,\n    public_network_transfer_bytes: 0.1,\n    root_branch_bytes_month: 0.35,\n    snapshot_storage_bytes_month: 0.09,\n  },\n  enterprise: {\n    child_branch_bytes_month: 0.35,\n    compute_unit_seconds: 0.222,\n    extra_branches_month: 1.5,\n    instant_restore_bytes_month: 0.2,\n    private_network_transfer_bytes: 0.01,\n    public_network_transfer_bytes: 0.1,\n    root_branch_bytes_month: 0.35,\n    snapshot_storage_bytes_month: 0.09,\n  },\n  launch: {\n    child_branch_bytes_month: 0.35,\n    compute_unit_seconds: 0.106,\n    extra_branches_month: 1.5,\n    instant_restore_bytes_month: 0.2,\n    private_network_transfer_bytes: 0,\n    public_network_transfer_bytes: 0.1,\n    root_branch_bytes_month: 0.35,\n    snapshot_storage_bytes_month: 0.09,\n  },\n  scale: {\n    child_branch_bytes_month: 0.35,\n    compute_unit_seconds: 0.222,\n    extra_branches_month: 1.5,\n    instant_restore_bytes_month: 0.2,\n    private_network_transfer_bytes: 0.01,\n    public_network_transfer_bytes: 0.1,\n    root_branch_bytes_month: 0.35,\n    snapshot_storage_bytes_month: 0.09,\n  },\n};\n\nexport interface CostLineItem {\n  /** The metric this line came from. */\n  id: ConsumptionMetricName;\n  label: string;\n  /** Billable amount after allowances, in the billing unit. */\n  quantity: number;\n  /**\n   * Total consumed before any allowance, in the same unit. Without it a\n   * line reading \"104 GB billable, 500 included\" hides whether the account\n   * is barely over its allowance or ten times over, which is the fact that\n   * decides whether to act.\n   */\n  used: number;\n  /** Amount the plan covers for free, in the same unit. */\n  included: number;\n  unit: string;\n  /** USD per billing unit. */\n  rate: number;\n  /** quantity x rate, in USD. */\n  cost: number;\n}\n\nexport interface CostEstimate {\n  items: CostLineItem[];\n  /** Sum of every line, in USD. Usage only; no plan base fee. */\n  total: number;\n  plan: ConsumptionPlan;\n}\n\nexport interface EstimateCostOptions {\n  /** Hours covered by the totals; needed for the branch allowance. */\n  hoursInPeriod?: number;\n  /** Override the per-project public transfer allowance, in GB. */\n  transferAllowanceGb?: number;\n  /** Drop lines that cost nothing. Default false, so zeroes stay visible. */\n  omitZero?: boolean;\n}\n\n/** Billable quantity for one metric, after the plan's allowance. */\nconst billableQuantity = (\n  metric: ConsumptionMetricName,\n  raw: number,\n  plan: ConsumptionPlan,\n  options: EstimateCostOptions\n): { quantity: number; included: number; used: number } => {\n  if (metric === \"public_network_transfer_bytes\") {\n    const gb = toGigabytes(raw);\n    const allowance = options.transferAllowanceGb ?? PUBLIC_TRANSFER_FREE_GB;\n\n    return {\n      included: Math.min(gb, allowance),\n      quantity: billableTransferGb(gb, allowance),\n      used: gb,\n    };\n  }\n\n  if (metric === \"extra_branches_month\") {\n    const hours = options.hoursInPeriod ?? 0;\n    const billableHours = billableBranchHours(raw, plan, hours);\n\n    return {\n      included: toBranchMonths(raw - billableHours),\n      quantity: toBranchMonths(billableHours),\n      used: toBranchMonths(raw),\n    };\n  }\n\n  const quantity = toBillingUnit(metric, raw);\n\n  return { included: 0, quantity, used: quantity };\n};\n\n/**\n * Turns raw metric totals into invoice-shaped line items. Allowances are\n * applied per metric, so the quantity shown is the quantity charged.\n *\n * Branch allowance is evaluated per hour by Neon's billing system; pass\n * `hoursInPeriod` and prefer hourly totals for the closest match.\n */\nexport const estimateCost = (\n  totals: ConsumptionTotals,\n  plan: ConsumptionPlan,\n  options: EstimateCostOptions = {}\n): CostEstimate => {\n  const rates = PLAN_RATES[plan];\n  const items: CostLineItem[] = [];\n\n  for (const metric of CONSUMPTION_METRICS) {\n    const raw = totals[metric];\n\n    if (raw === undefined) {\n      continue;\n    }\n\n    const { included, quantity, used } = billableQuantity(\n      metric,\n      raw,\n      plan,\n      options\n    );\n    const rate = rates[metric];\n    const cost = quantity * rate;\n\n    if (options.omitZero && cost === 0) {\n      continue;\n    }\n\n    items.push({\n      cost,\n      id: metric,\n      included,\n      label: METRIC_LABELS[metric],\n      quantity,\n      rate,\n      unit: METRIC_BILLING_UNIT[metric],\n      used,\n    });\n  }\n\n  return {\n    items,\n    plan,\n    total: items.reduce((sum, item) => sum + item.cost, 0),\n  };\n};\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:lib"
}