mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The five days of upstream evolution since the 2026-08-01 import land through bin/debrand: new query schema, billing counters, ducklake compaction, the desktop sandbox default, capture evolution. Ours' invariants hold everywhere they met it: IAM-native auth only, no ee, no upsell surfaces, one sessions model, org-is-the-tenant, v1-first serving, fail-closed capture. Crates and workflows the fork retired stay retired; the Lemon prefix finishes dissolving into elements.
404 lines
20 KiB
Python
404 lines
20 KiB
Python
"""Signals billing — a flat credit charge per report that ships an implementation PR.
|
|
|
|
Signals is billed on outcomes, not LLM spend: each signal report whose implementation task
|
|
opens a pull request is charged a flat number of credits, once. The chargeable moment is
|
|
deterministic — the first implementation `TaskRun` with a `pr_url` set — so a report is
|
|
billed exactly once, in the period that PR first appeared, regardless of any later status
|
|
changes, re-judgements, or additional runs.
|
|
|
|
The PR↔report link lives on the `SignalReportTask` bridge (relationship="implementation"); the
|
|
PR URL itself is written to `TaskRun.output['pr_url']`. There is no artefact that records the
|
|
implementation task or its PR, so the query is rooted on that bridge, not on artefacts.
|
|
|
|
Because this is a billing source it fails closed: a run only bills when its PR URL is a GitHub
|
|
URL and the run, task, bridge, and report teams all agree, so malformed bridge rows never
|
|
produce a charge.
|
|
|
|
Credits use the same unit as ai_credits: 1 credit = $0.01, so the flat $15 charge is 1500
|
|
credits.
|
|
|
|
Two mechanisms remove a report from billing:
|
|
|
|
- **Refunds** (retrospective, `SignalReportRefund`): an `excluded`-path refund makes the usage
|
|
query skip the report entirely — committed strictly on the same UTC day as the first billable
|
|
PR run, so no usage send (all of which happen ≥3h45m into the next UTC day) can ever observe
|
|
the exclusion flipping. A `credited`-path refund is deliberately NOT consulted here: usage
|
|
stays truthful and the money comes back as a billing-service credit.
|
|
- **Exemptions** (prospective, `SignalReport.billing_exempt_reason`): Insights-system-origin
|
|
reports are marked never-billable before their billable moment exists, so they never enter
|
|
usage (nor, via the same query driving quota `todays_usage`, consume a free-tier slot).
|
|
|
|
Emergent semantic to be aware of: because of the `billed_earlier` idempotency check below, a
|
|
report whose first billable PR run has been refunded can NEVER be billed again — a later, second
|
|
PR run on the same report finds the first run before the period and is skipped, and the first
|
|
run itself is refund-excluded (excluded path) or already handled by a credit (credited path).
|
|
This is why restoring a refunded report is blocked at the API layer: refund → restore → new PR
|
|
would otherwise be repeatable free work.
|
|
"""
|
|
|
|
import uuid
|
|
from collections import defaultdict
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING, NamedTuple
|
|
|
|
from django.db.models import F, OuterRef, QuerySet, Subquery, Sum
|
|
from django.utils import timezone
|
|
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from insights.models.organization import BillingPeriod
|
|
|
|
from products.signals.backend.artefact_schemas import TASK_RUN_TYPE_IMPLEMENTATION
|
|
from products.signals.backend.enums import SignalSourceProduct
|
|
from products.signals.backend.models import SignalReport, SignalReportRefund, SignalReportTask, SignalScoutRun
|
|
from products.signals.backend.scout_harness.lazy_seed import scout_skill_row_origin
|
|
from products.skills.backend.models.skills import LLMSkill
|
|
|
|
if TYPE_CHECKING:
|
|
from insights.models.organization import Organization
|
|
|
|
_IMPLEMENTATION = TASK_RUN_TYPE_IMPLEMENTATION
|
|
|
|
# Only PRs hosted on GitHub are billable. The PR URL is GitHub's `html_url`
|
|
# (https://github.com/owner/repo/pull/N), so validate the host prefix to avoid charging
|
|
# for malformed or non-GitHub values written into `output.pr_url`.
|
|
_GITHUB_PR_URL_PREFIX = "https://github.com/"
|
|
|
|
SIGNALS_CREDITS_PER_DOLLAR = 100 # 1 credit = $0.01, matching ai_credits
|
|
|
|
# Flat credits charged once per report whose implementation shipped a PR ($15).
|
|
SIGNALS_CREDITS_PER_REPORT_WITH_PR = 15 * SIGNALS_CREDITS_PER_DOLLAR # 1500
|
|
|
|
|
|
def _bridges_with_pr_run(**run_created_at: datetime) -> QuerySet[SignalReportTask]:
|
|
"""Implementation bridges whose task shipped a billable PR run matching `run_created_at`.
|
|
|
|
Rooted on the signals-owned `SignalReportTask` bridge and traversing to runs via the
|
|
`task__runs` relation, so the query never imports the tasks product's internals — it stays
|
|
behind the tasks public interface. Postgres is free to drive the join from the run
|
|
`created_at` index regardless, so the period scan stays bounded by PRs shipped, not by the
|
|
number of bridges.
|
|
|
|
Fail closed: a bridge only counts when one of its runs carries a GitHub PR URL within the
|
|
given `created_at` bound and the four teams in the chain — run, task, bridge, and report —
|
|
all agree. A malformed bridge whose teams disagree is excluded rather than charged to
|
|
whichever team_id happened to be on it. The run-level conditions sit in one `filter()` so
|
|
they all resolve against the same `TaskRun` row.
|
|
"""
|
|
return SignalReportTask.objects.filter(
|
|
relationship=_IMPLEMENTATION,
|
|
task__runs__output__pr_url__startswith=_GITHUB_PR_URL_PREFIX,
|
|
# Fail closed on team disagreement across run / task / bridge / report.
|
|
task__team_id=F("team_id"),
|
|
report__team_id=F("team_id"),
|
|
task__runs__team_id=F("team_id"),
|
|
# `output__pr_url__startswith` only matches present, string-typed values, so no
|
|
# separate isnull / empty-string guard is needed.
|
|
**run_created_at,
|
|
)
|
|
|
|
|
|
class BillingExemptionError(Exception):
|
|
"""A report can no longer be exempted — a billable PR run already exists (use a refund)."""
|
|
|
|
|
|
# Insights-system scout skills whose reports must never bill — users must not pay for PRs that fix
|
|
# problems in Insights's own systems. v1 policy is this code constant; the report→scout link is the
|
|
# runs' `emitted_report_ids` tally (report-channel scouts author their reports directly).
|
|
BILLING_EXEMPT_SCOUT_SKILLS: dict[str, str] = {
|
|
"signals-scout-health-checks": SignalReport.BillingExemptReason.INSIGHTS_HEALTH_CHECK,
|
|
}
|
|
|
|
# Same policy for the signal channel: sources owned by Insights systems (e.g. the temporal
|
|
# health-checks pipeline, `insights/temporal/health_checks/signal_emitter.py`). Per-signal
|
|
# source_product never reaches Postgres, so this map is enforced where the report row is
|
|
# formed — the grouping activity stamps `billing_exempt_reason` at creation. Formation-time
|
|
# only: an exempt-origin signal joining an existing report never flips it (and vice versa),
|
|
# mirroring `mark_report_billing_exempt`'s first-reason-wins freeze.
|
|
BILLING_EXEMPT_SOURCE_PRODUCTS: dict[str, str] = {
|
|
SignalSourceProduct.HEALTH_CHECKS: SignalReport.BillingExemptReason.INSIGHTS_HEALTH_CHECK,
|
|
SignalSourceProduct.ENGINEERING_ANALYTICS: SignalReport.BillingExemptReason.INSIGHTS_SYSTEM,
|
|
}
|
|
|
|
|
|
def _scout_skill_is_canonical(team_id: int, skill_name: str) -> bool:
|
|
"""Whether the team's row for `skill_name` is still the Insights-shipped scout, byte for byte.
|
|
|
|
`skill_name` alone doesn't attest Insights-system origin: a team can edit a seeded scout in
|
|
place and the diverged row keeps its canonical name, which would let a repurposed fork mint
|
|
permanent exemptions. `scout_skill_row_origin` settles it by content hash against the
|
|
fingerprint stamped at seed time. Fails closed — no row, no proof → billable.
|
|
"""
|
|
skill = LLMSkill.objects.filter(team_id=team_id, name=skill_name, deleted=False, is_latest=True).first()
|
|
return skill is not None and scout_skill_row_origin(skill) == "canonical"
|
|
|
|
|
|
def system_billing_exempt_reason(team_id: int, report_id: str | uuid.UUID) -> str | None:
|
|
"""The exemption reason system policy assigns to a report, or None for normal reports.
|
|
|
|
Pure Postgres (no Datastore), so it is usable under the auto-start row lock: a report is
|
|
exempt-origin when an exempt scout skill's run authored it via `emit_report` — and the
|
|
team's row for that skill is still the canonical Insights-shipped content (a team-edited
|
|
fork keeps the name but is no longer Insights's system, so its reports stay billable).
|
|
"""
|
|
for skill_name, reason in BILLING_EXEMPT_SCOUT_SKILLS.items():
|
|
if (
|
|
SignalScoutRun.objects.for_team(team_id)
|
|
.filter(skill_name=skill_name, emitted_report_ids__contains=[str(report_id)])
|
|
.exists()
|
|
) and _scout_skill_is_canonical(team_id, skill_name):
|
|
return reason
|
|
return None
|
|
|
|
|
|
def mark_report_billing_exempt(report: SignalReport, reason: str) -> bool:
|
|
"""Mark `report` never-billable, enforcing the prospective-only freeze rule (hard).
|
|
|
|
Returns False (no-op) when the report already carries an exemption — the first reason wins,
|
|
exemptions are never rewritten. Raises `BillingExemptionError` when the report already has a
|
|
billable PR run: anything already billable is a refund, never a late exemption, so no usage
|
|
report can ever observe this field flipping.
|
|
|
|
Persists only the exemption column (deliberately not bumping `updated_at` — a billing-internal
|
|
stamp must not resurface the report in recency-ordered lists). Callers needing atomicity with
|
|
other writes should hold the report row lock; the auto-start hook does.
|
|
"""
|
|
if reason not in SignalReport.BillingExemptReason.values:
|
|
raise ValueError(f"Unknown billing exemption reason: {reason!r}")
|
|
if report.billing_exempt_reason:
|
|
return False
|
|
if first_billable_pr_run_at(report.id) is not None:
|
|
raise BillingExemptionError(
|
|
f"Report {report.id} already has a billable PR run; use a refund instead of a billing exemption."
|
|
)
|
|
report.billing_exempt_reason = reason
|
|
report.save(update_fields=["billing_exempt_reason"])
|
|
return True
|
|
|
|
|
|
class FirstBillablePrRun(NamedTuple):
|
|
created_at: datetime
|
|
pr_url: str
|
|
|
|
|
|
def first_billable_pr_run(report_id: str | uuid.UUID) -> FirstBillablePrRun | None:
|
|
"""The report's billable moment: its first implementation run with a GitHub PR URL (with that
|
|
run's `created_at` and `pr_url`), or None if it has never shipped one.
|
|
|
|
Single source of truth for "when did this report become chargeable" — the refund action uses
|
|
it for eligibility, snapshots, and the UTC-day path decision, and the exemption freeze rule
|
|
uses it to refuse late exemptions. It applies the exact same fail-closed filters as the usage
|
|
query, so the two can never disagree about whether a billable run exists.
|
|
"""
|
|
row = (
|
|
_bridges_with_pr_run()
|
|
.filter(report_id=report_id)
|
|
# Both columns resolve against the TaskRun join the filter established, so the earliest
|
|
# billable run's timestamp and URL come from the same row.
|
|
.order_by("task__runs__created_at")
|
|
.values_list("task__runs__created_at", "task__runs__output__pr_url")
|
|
.first()
|
|
)
|
|
if row is None:
|
|
return None
|
|
return FirstBillablePrRun(created_at=row[0], pr_url=row[1])
|
|
|
|
|
|
def first_billable_pr_run_at(report_id: str | uuid.UUID) -> datetime | None:
|
|
"""`first_billable_pr_run`, timestamp only."""
|
|
run = first_billable_pr_run(report_id)
|
|
return run.created_at if run else None
|
|
|
|
|
|
def report_pr_is_merged(report_id: str | uuid.UUID, pr_url: str) -> bool:
|
|
"""Whether *this* PR of the report merged, per the tasks GitHub webhook.
|
|
|
|
Reads `output.pr_merged`, the flag the webhook persists when a PR merges — the factual record of
|
|
a merge, independent of report status. A report can now reach RESOLVED without a merged PR (a
|
|
user or agent can resolve it directly), so status alone no longer attests a merge.
|
|
|
|
Scoped to one `pr_url` rather than the whole report, because the caller is deciding about a
|
|
specific PR: the refund reverses the charge for the billable run's PR, and it's that PR which
|
|
must be closed if it never merged. A report-level check would let an unrelated later PR that did
|
|
merge vouch for the refunded one, leaving the refunded PR open.
|
|
|
|
Fail closed like `_bridges_with_pr_run`: the PR URL, the merge flag, and the four team checks all
|
|
sit in one `filter()` so they resolve against the same `TaskRun` row.
|
|
"""
|
|
return SignalReportTask.objects.filter(
|
|
relationship=_IMPLEMENTATION,
|
|
report_id=report_id,
|
|
task__team_id=F("team_id"),
|
|
report__team_id=F("team_id"),
|
|
task__runs__team_id=F("team_id"),
|
|
task__runs__output__pr_url=pr_url,
|
|
task__runs__output__pr_merged=True,
|
|
).exists()
|
|
|
|
|
|
def annotate_first_billable_pr_run_at(queryset: QuerySet[SignalReport]) -> QuerySet[SignalReport]:
|
|
"""Annotate each report with `first_billable_pr_run_at` (its billable moment, batched form of
|
|
`first_billable_pr_run`), NULL when it never shipped a billable PR run. Applies the same
|
|
fail-closed filters as the usage query, so list-rendered refund eligibility can never
|
|
disagree with what the refund action would accept."""
|
|
earliest = (
|
|
_bridges_with_pr_run()
|
|
.filter(report_id=OuterRef("id"))
|
|
.order_by("task__runs__created_at")
|
|
.values("task__runs__created_at")[:1]
|
|
)
|
|
return queryset.annotate(first_billable_pr_run_at=Subquery(earliest))
|
|
|
|
|
|
# Why a report can't be refunded right now (`refund_ineligibility_reason`); None = refundable.
|
|
REFUND_INELIGIBLE_ALREADY_REFUNDED = "already_refunded"
|
|
REFUND_INELIGIBLE_BILLING_EXEMPT = "billing_exempt"
|
|
REFUND_INELIGIBLE_NO_BILLABLE_PR = "no_billable_pr"
|
|
REFUND_INELIGIBLE_OUT_OF_PERIOD = "out_of_period"
|
|
|
|
REFUND_INELIGIBILITY_REASONS = (
|
|
REFUND_INELIGIBLE_ALREADY_REFUNDED,
|
|
REFUND_INELIGIBLE_BILLING_EXEMPT,
|
|
REFUND_INELIGIBLE_NO_BILLABLE_PR,
|
|
REFUND_INELIGIBLE_OUT_OF_PERIOD,
|
|
)
|
|
|
|
|
|
def refund_ineligibility_reason(
|
|
*,
|
|
has_refund: bool,
|
|
billing_exempt: bool,
|
|
billable_run_at: datetime | None,
|
|
period: BillingPeriod,
|
|
) -> str | None:
|
|
"""Why a report can't be refunded right now, or None when a refund would be accepted.
|
|
|
|
The single eligibility decision, shared by the refund action (which maps each reason to its
|
|
400/idempotent response) and the report serializer (which exposes the reason so the UI can
|
|
disable the Refund button) — the button state can never drift from what the POST enforces.
|
|
"""
|
|
if has_refund:
|
|
return REFUND_INELIGIBLE_ALREADY_REFUNDED
|
|
if billing_exempt:
|
|
return REFUND_INELIGIBLE_BILLING_EXEMPT
|
|
if billable_run_at is None:
|
|
return REFUND_INELIGIBLE_NO_BILLABLE_PR
|
|
if not (period.start <= billable_run_at < period.end):
|
|
return REFUND_INELIGIBLE_OUT_OF_PERIOD
|
|
return None
|
|
|
|
|
|
def credited_refund_credits_for_org(organization_id: str | uuid.UUID, begin: datetime, end: datetime) -> int:
|
|
"""Sum of `credits` over the org's credited-path refunds whose refunded PR run falls in
|
|
`[begin, end)` — the amount the quota check offsets so a credited refund frees the free-tier
|
|
slot, not just the money. Keyed on `pr_run_created_at` (not refund creation) so the offset
|
|
lines up with the period the usage was billed in. Unscoped: deliberately org-wide."""
|
|
return (
|
|
SignalReportRefund.objects.unscoped()
|
|
.filter(
|
|
team__organization_id=organization_id,
|
|
billing_path=SignalReportRefund.BillingPath.CREDITED,
|
|
pr_run_created_at__gte=begin,
|
|
pr_run_created_at__lt=end,
|
|
)
|
|
.aggregate(total=Sum("credits"))["total"]
|
|
or 0
|
|
)
|
|
|
|
|
|
def current_billing_period_bounds(organization: "Organization") -> BillingPeriod:
|
|
"""The org's current billing period `[start, end)`, falling back to the current UTC calendar
|
|
month when billing hasn't populated `organization.usage["period"]` (e.g. self-hosted or a
|
|
just-created org). Refund eligibility and the org-wide refund summary both key off this."""
|
|
period = organization.current_billing_period
|
|
if period is not None:
|
|
return period
|
|
now = timezone.now()
|
|
start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
|
return BillingPeriod(start=start, end=start + relativedelta(months=1))
|
|
|
|
|
|
def get_signals_billing_credits_by_team(
|
|
begin: datetime, end: datetime, organization_id: str | uuid.UUID | None = None
|
|
) -> list[tuple[int, int]]:
|
|
"""Signals credits used per team in `[begin, end)`.
|
|
|
|
A report is billable in this period when the first implementation PR for it appeared in the
|
|
window — an implementation `TaskRun` with a `pr_url` was created in `[begin, end)` and no
|
|
such run exists before `begin`. Each billable report is charged a flat
|
|
`SIGNALS_CREDITS_PER_REPORT_WITH_PR`. Returns `[(team_id, credits), ...]` for teams with
|
|
non-zero usage only. `organization_id` narrows the scan to one org (the widget's live-count
|
|
path); usage reporting passes nothing and stays fleet-wide.
|
|
|
|
The entry scan is bounded by PRs shipped in the period (via the `created_at` +
|
|
`output__pr_url` indexes), not by the total number of reports, task runs, or teams.
|
|
"""
|
|
# Reports whose implementation produced a billable PR within this period, mapped to the team
|
|
# that owns the report. The teams are guaranteed equal by `_bridges_with_pr_run()`, so the
|
|
# bridge's team_id is the report's team.
|
|
bridges = _bridges_with_pr_run(
|
|
task__runs__created_at__gte=begin,
|
|
task__runs__created_at__lt=end,
|
|
)
|
|
if organization_id is not None:
|
|
bridges = bridges.filter(team__organization_id=organization_id)
|
|
report_team: dict[uuid.UUID, int] = {}
|
|
for report_id, team_id in bridges.values_list("report_id", "team_id"):
|
|
report_team.setdefault(report_id, team_id)
|
|
|
|
if not report_team:
|
|
return []
|
|
|
|
report_ids = list(report_team)
|
|
|
|
# Exclude reports whose first implementation PR predates this period — they were billed in an
|
|
# earlier period. This is what makes billing idempotent across re-runs and prevents
|
|
# double-charging when a report ships more PR runs later.
|
|
billed_earlier = set(
|
|
_bridges_with_pr_run(task__runs__created_at__lt=begin)
|
|
.filter(report_id__in=report_ids)
|
|
.values_list("report_id", flat=True)
|
|
)
|
|
|
|
# Excluded-path refunds: billing must never learn these reports existed. Deterministic across
|
|
# re-sends by the UTC-day path rule (see module docstring). Credited-path refunds are
|
|
# deliberately absent — their usage stays truthful. Unscoped: this aggregates across all teams
|
|
# outside any request context.
|
|
refund_excluded = set(
|
|
SignalReportRefund.objects.unscoped()
|
|
.filter(report_id__in=report_ids, billing_path=SignalReportRefund.BillingPath.EXCLUDED)
|
|
.values_list("report_id", flat=True)
|
|
)
|
|
|
|
# Billing-exempt reports (Insights-system origins) never bill; the reason is frozen before the
|
|
# billable moment exists, so this is deterministic across re-sends too.
|
|
billing_exempt = set(
|
|
# nosemgrep: idor-lookup-without-team (ids come from the team-grouped billing bridge query above; usage aggregation is deliberately cross-team)
|
|
SignalReport.objects.filter(id__in=report_ids, billing_exempt_reason__isnull=False).values_list("id", flat=True)
|
|
)
|
|
|
|
skipped = billed_earlier | refund_excluded | billing_exempt
|
|
|
|
totals: dict[int, int] = defaultdict(int)
|
|
for report_id, team_id in report_team.items():
|
|
if report_id in skipped:
|
|
continue
|
|
totals[team_id] += SIGNALS_CREDITS_PER_REPORT_WITH_PR
|
|
|
|
return list(totals.items())
|
|
|
|
|
|
def period_billable_credits_for_org(organization_id: str | uuid.UUID, *, period: BillingPeriod) -> int:
|
|
"""The org's billable signals credits for `period` per the exact usage-report rules —
|
|
including PRs created today that haven't been reported to billing yet.
|
|
|
|
Powers the inbox usage widget's live PR count: the frontend takes the max of this and
|
|
billing's recorded usage, so a fresh PR counts immediately and a same-UTC-day (excluded-path)
|
|
refund visibly un-counts it. Display-only — usage reports never read this; credited-path
|
|
refunds stay included here (usage is truthful) and are netted separately by the widget.
|
|
"""
|
|
return sum(
|
|
credits
|
|
for _, credits in get_signals_billing_credits_by_team(period.start, period.end, organization_id=organization_id)
|
|
)
|