Skip to content

feat(organizations): spending alerts with monthly spending threshold - #5457

Open
jrf0110 wants to merge 6 commits into
mainfrom
feature/monthly-spending-threshold
Open

feat(organizations): spending alerts with monthly spending threshold#5457
jrf0110 wants to merge 6 commits into
mainfrom
feature/monthly-spending-threshold

Conversation

@jrf0110

@jrf0110 jrf0110 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the monthly spending threshold spec, and revises the spec itself in one
important way: an organization may create as many alerts as it wants instead of
a single monthly threshold setting.

The surface is a general Account → Alerts page. Creating an alert starts from an
alert-type dropdown that defaults to Monthly Spending and shows a disabled
More coming soon option; changing the type swaps in that type's editor. As with
the Group Policy editor, a type registry plus DrawerStack means a second alert
type needs no changes to the shell.

Why the multiplicity change matters

Allowing many alerts is not just a UI change. It invalidates the spec's original
storage and deduplication model:

  • delivery identity must be alert-scoped (alert + period occurrence + recipient digest + channel), otherwise two independently configured alerts suppress each
    other's email
  • the 10-recipient limit is per alert, not per organization
  • alerts need stable identities and their own lifecycle, so they get dedicated
    tables rather than organizations.settings
  • alert type is immutable; choosing a different type means a new alert

What's implemented

Persistenceorganization_alerts and organization_alert_deliveries
(migration 0231). Delivery rows never store an address, only a keyed digest; the
address is re-read from configuration at send time.

Domain — typed alert union, versioned calendar_month_utc period resolution to
a stable occurrence identity and half-open UTC interval, exact whole-cent threshold
conversion with no floating-point drift, recipient normalization/dedup with a 1–10
bound (0 allowed while disabled so a disclosure can always be removed).

APIorganizations.alerts.{list,create,update,setEnabled,archive} behind the
existing organization billing authority, including inherited parent-over-child
authority. Enterprise entitlement is required to create, enable, or expand; disable,
archive, and recipient removal deliberately remain available after entitlement loss.
Edits use optimistic concurrency on configuration version. Audit entries carry the
alert ID and recipient counts, never addresses.

UI — billing-gated sidebar entry and server-authorized route, paginated alert
list, the Monthly Spending editor (threshold, up to 10 recipients, UTC month
explanation, informational/no-cap framing, required disclosure confirmation, inline
errors), and inline disable/archive confirmation inside the drawer. The existing low
balance alert is presented alongside as a clearly separate card, reusing its
implementation.

Delivery — hourly cron evaluates enabled alerts of live Enterprise organizations
in bounded keyset pages, aggregates spend once per organization-period and
compares each alert independently, claims delivery under a per-alert-period advisory
lock before any provider call, re-reads eligibility before sending, and cancels stale
work. A definitive pre-submission failure stays retryable; an ambiguous provider
outcome is retained and never retried automatically.

Lifecycle — leaving Enterprise disables alerts and cancels claimed work without
re-enabling on a later upgrade; organization deletion removes recipient addresses,
which matters because deletion is soft and no FK cascade fires.

Validation

  • pnpm format, pnpm typecheck, pnpm dependency-cycle-check clean
  • 109 alert tests pass (domain, router, presentation, evaluator, lifecycle), plus the
    packages/db schema/migration-consistency suite and drizzle-kit check
  • pnpm lint reports only 2 pre-existing errors in untouched
    services/user-data-export/src/index.ts
  • known pre-existing unrelated failure: the migration 0204 case in
    packages/db/src/schema.test.ts reads a migration file that was renumbered away

Follow-ups

  • provider_message_id is not yet written; send() does not surface a Mailgun ID
  • MAX_DISPATCH_PER_RUN sends serially, worth confirming against the route's
    effective maxDuration once real volume exists

@kilo-code-bot

kilo-code-bot Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: 3 Issues Found | Recommendation: Address before merge

Executive Summary

Low-balance crossings that land at a negative org balance cannot be claimed, and the new Measure/Group selects render behind the alerts drawer so group-scoped monthly spending cannot be configured in the UI.

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 0

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
apps/web/src/lib/organizations/alerts/low-balance/low-balance-evaluator.ts 182 Negative org balances cannot be claimed, so a crossing that lands below zero never emails
apps/web/src/components/organizations/alerts/monthly-spending/MonthlySpendingAlertEditor.tsx 214 Measure dropdown renders behind the alerts drawer
apps/web/src/components/organizations/alerts/monthly-spending/MonthlySpendingAlertEditor.tsx 242 Group picker renders behind the alerts drawer
Files Reviewed (40 files)
  • apps/web/src/components/organizations/alerts/alert-presentation.test.ts
  • apps/web/src/components/organizations/alerts/alert-presentation.ts
  • apps/web/src/components/organizations/alerts/drawer/AlertEditorPanel.tsx
  • apps/web/src/components/organizations/alerts/low-balance/LowBalanceAlertEditor.tsx
  • apps/web/src/components/organizations/alerts/low-balance/low-balance-form.ts
  • apps/web/src/components/organizations/alerts/low-balance/low-balance.definition.client.ts
  • apps/web/src/components/organizations/alerts/monthly-spending/MonthlySpendingAlertEditor.tsx - 2 issues
  • apps/web/src/components/organizations/alerts/monthly-spending/monthly-spending-form.test.ts
  • apps/web/src/components/organizations/alerts/monthly-spending/monthly-spending-form.ts
  • apps/web/src/components/organizations/alerts/monthly-spending/monthly-spending.definition.client.ts
  • apps/web/src/components/organizations/alerts/registry.client.ts
  • apps/web/src/components/organizations/alerts/types.ts
  • apps/web/src/emails/AGENTS.md
  • apps/web/src/emails/organizationLowBalanceAlert.html
  • apps/web/src/lib/ai-gateway/processUsage.ts
  • apps/web/src/lib/email.ts
  • apps/web/src/lib/exa-usage.ts
  • apps/web/src/lib/kilo-pass-org/consumption.ts
  • apps/web/src/lib/organizations/alerts/alert-deliveries.ts
  • apps/web/src/lib/organizations/alerts/alert-lifecycle.test.ts
  • apps/web/src/lib/organizations/alerts/alert-thresholds.ts
  • apps/web/src/lib/organizations/alerts/low-balance/low-balance-evaluator.test.ts
  • apps/web/src/lib/organizations/alerts/low-balance/low-balance-evaluator.ts - 1 issue
  • apps/web/src/lib/organizations/alerts/low-balance/low-balance.schema.test.ts
  • apps/web/src/lib/organizations/alerts/low-balance/low-balance.schema.ts
  • apps/web/src/lib/organizations/alerts/monthly-spending/monthly-spending-evaluator.test.ts
  • apps/web/src/lib/organizations/alerts/monthly-spending/monthly-spending-evaluator.ts
  • apps/web/src/lib/organizations/alerts/monthly-spending/monthly-spending.schema.test.ts
  • apps/web/src/lib/organizations/alerts/monthly-spending/monthly-spending.schema.ts
  • apps/web/src/lib/organizations/alerts/organization-alerts.server.ts
  • apps/web/src/lib/organizations/alerts/organization-alerts.test.ts
  • apps/web/src/lib/organizations/alerts/organization-alerts.ts
  • apps/web/src/lib/organizations/organization-usage.test.ts
  • apps/web/src/lib/organizations/organization-usage.ts
  • apps/web/src/routers/organizations/organization-alerts-router.test.ts
  • packages/db/src/migrations/0234_glossy_silver_samurai.sql
  • packages/db/src/migrations/0235_slimy_doctor_octopus.sql
  • packages/db/src/migrations/meta/_journal.json
  • packages/db/src/schema.test.ts
  • packages/db/src/schema.ts
Previous Review Summaries (2 snapshots, latest commit 5286814)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 5286814)

Status: No Issues Found | Recommendation: Merge

Executive Summary

Reviewed the monthly spending alerts implementation (schema, delivery/claim, API/auth, UI, and spec alignment) at 52868143 with high confidence and no high-severity defects.

Files Reviewed (49 files)
  • .kilo/skills/specs/SKILL.md
  • .specs/monthly-spending-thresholds.md
  • apps/web/src/app/(app)/components/OrganizationAppSidebar.tsx
  • apps/web/src/app/(app)/organizations/[id]/alerts/page.tsx
  • apps/web/src/app/api/cron/organization-alerts/route.ts
  • apps/web/src/components/organizations/OrganizationEmailPreferencesCard.tsx
  • apps/web/src/components/organizations/alerts/AlertEditorFooter.tsx
  • apps/web/src/components/organizations/alerts/LowBalanceAlertCard.tsx
  • apps/web/src/components/organizations/alerts/OrganizationAlertsPage.tsx
  • apps/web/src/components/organizations/alerts/alert-presentation.test.ts
  • apps/web/src/components/organizations/alerts/alert-presentation.ts
  • apps/web/src/components/organizations/alerts/drawer/AlertEditorPanel.tsx
  • apps/web/src/components/organizations/alerts/drawer/OrganizationAlertsDrawerStack.tsx
  • apps/web/src/components/organizations/alerts/drawer/types.ts
  • apps/web/src/components/organizations/alerts/monthly-spending/MonthlySpendingAlertEditor.tsx
  • apps/web/src/components/organizations/alerts/monthly-spending/monthly-spending-form.test.ts
  • apps/web/src/components/organizations/alerts/monthly-spending/monthly-spending-form.ts
  • apps/web/src/components/organizations/alerts/monthly-spending/monthly-spending.definition.client.ts
  • apps/web/src/components/organizations/alerts/registry.client.ts
  • apps/web/src/components/organizations/alerts/types.ts
  • apps/web/src/components/organizations/alerts/useOrganizationAlerts.ts
  • apps/web/src/components/organizations/spending-alerts-form.ts
  • apps/web/src/emails/AGENTS.md
  • apps/web/src/emails/monthlySpendingAlert.html
  • apps/web/src/lib/email.ts
  • apps/web/src/lib/organizations/alerts/alert-deliveries.ts
  • apps/web/src/lib/organizations/alerts/alert-lifecycle.test.ts
  • apps/web/src/lib/organizations/alerts/alert-lifecycle.ts
  • apps/web/src/lib/organizations/alerts/alert-periods.test.ts
  • apps/web/src/lib/organizations/alerts/alert-periods.ts
  • apps/web/src/lib/organizations/alerts/alert-recipients.test.ts
  • apps/web/src/lib/organizations/alerts/alert-recipients.ts
  • apps/web/src/lib/organizations/alerts/monthly-spending/monthly-spending-evaluator.test.ts
  • apps/web/src/lib/organizations/alerts/monthly-spending/monthly-spending-evaluator.ts
  • apps/web/src/lib/organizations/alerts/monthly-spending/monthly-spending.schema.test.ts
  • apps/web/src/lib/organizations/alerts/monthly-spending/monthly-spending.schema.ts
  • apps/web/src/lib/organizations/alerts/organization-alerts.server.ts
  • apps/web/src/lib/organizations/alerts/organization-alerts.test.ts
  • apps/web/src/lib/organizations/alerts/organization-alerts.ts
  • apps/web/src/lib/organizations/organization-seats.ts
  • apps/web/src/lib/organizations/organizations.ts
  • apps/web/src/routers/organizations/organization-alerts-router.test.ts
  • apps/web/src/routers/organizations/organization-alerts-router.ts
  • apps/web/src/routers/organizations/organization-router.ts
  • apps/web/vercel.json
  • packages/db/src/migrations/0231_lively_cargill.sql
  • packages/db/src/schema-types.ts
  • packages/db/src/schema.test.ts
  • packages/db/src/schema.ts

Previous review (commit 11bf108)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (2 files)
  • .kilo/skills/specs/SKILL.md
  • .specs/monthly-spending-thresholds.md

Reviewed by grok-4.6 · Input: 204.6K · Output: 21.4K · Cached: 881.9K

Review guidance: REVIEW.md from base branch main

Implements the monthly spending threshold PRD as a general, extensible
Alerts surface rather than a single per-organization setting.

An organization may create any number of alerts. Each alert has an
immutable type, a versioned type-specific configuration, an enabled/
disabled/archived lifecycle, and a configuration version used for
optimistic concurrency. Delivery is claimed durably per alert, period
occurrence, recipient and channel, so two alerts never suppress each
other's email and no recipient is notified twice for one period.

- persistence: `organization_alerts` and `organization_alert_deliveries`,
  with recipient identity stored only as a keyed digest
- domain: typed alert union, versioned `calendar_month_utc` period
  resolution, exact whole-cent threshold conversion, recipient
  normalization and a 1-10 per-alert bound
- API: cursor-paginated list plus create/update/setEnabled/archive,
  gated by organization billing authority; Enterprise entitlement is
  required to create, enable or expand, while disable, archive and
  recipient removal stay available after entitlement loss
- UI: Account > Alerts page with a DrawerStack editor, an alert type
  dropdown defaulting to Monthly Spending with a disabled "More coming
  soon" option, and a type registry so a second type needs no shell
  changes; the existing low balance alert is surfaced alongside it
- delivery: hourly cron evaluates enabled alerts in bounded keyset
  pages, aggregates spend once per organization-period, claims under a
  per-alert-period advisory lock, re-reads eligibility before sending,
  and distinguishes retryable pre-submission failures from ambiguous
  provider outcomes that must never be retried
- lifecycle: leaving Enterprise disables alerts and cancels claimed
  work; organization deletion removes recipient addresses
Brings `main` up to date on this branch and adds the implementation of
the monthly spending threshold spec. The spec itself is revised in the
merged commit: alerts are a collection rather than one setting per
organization, so delivery identity, recipient admission and the editor
UX are all alert-scoped.
@jrf0110 jrf0110 changed the title docs(specs): define monthly spending thresholds feat(organizations): spending alerts with monthly spending threshold Aug 26, 2026
The per-alert-period advisory lock only protected the 10-recipient
admission cap. At-most-once delivery per recipient and period is
guaranteed by the unique index on delivery identity, which needs no
serialization: a concurrent evaluator's overlapping insert is absorbed
by ON CONFLICT.

The cap is a fanout bound on mid-period recipient churn rather than a
safety invariant, and the configuration schema already limits an alert
to 10 recipients, so a single sweep cannot exceed it. Overlapping
evaluation may now admit a few extra addresses for one period, which
the spec records as acceptable. Claiming is a single statement, so the
surrounding transaction is gone too.

Adds a test that concurrent claims produce exactly one row per
recipient, since that invariant now stands alone.
Resolves conflicts from concurrent work on main:

- `packages/db/src/schema.ts`: main added `organization_domain_claims`
  directly after the organizations table, where the alert tables also
  landed. Both are kept.
- migrations: main took slots 0231 and 0232, so the branch-local
  migration and its snapshot were discarded and regenerated as
  0233_condemned_hardball against the merged schema. The generated SQL
  contains only the two alert tables, with no drift and no destructive
  statements.
- `.kilo/skills/specs/SKILL.md`: keeps both new index entries, spending
  alerts and verified-domain auto-join.
…spending

- Recreate the legacy low-balance alert as a new low_balance type in the
  collection-backed alerts system (organization_alerts), evaluated
  just-in-time from every usage-mutation call site (token usage ingest, Exa,
  AI Gateway) rather than a periodic sweep, since a balance change has
  exactly one call site unlike accumulated spend. Coexists with the legacy
  organizations.settings-based low-balance alert; nothing about it changes.

- Generalize organization_alert_deliveries.measured_spend_microdollars to
  measured_value_microdollars (direction-agnostic) so low_balance's
  below-threshold semantics can share the claim/dispatch machinery with
  monthly_spending's at-or-above semantics. Two migrations split the rename
  into an unambiguous add then drop.

- Add scope to monthly spending alerts: measure the whole organization or a
  specific organization group, via a live-membership join against
  organization_group_memberships at evaluation time. A scope naming a
  deleted group is treated as invalid rather than silently measured as zero
  spend.

- Extract shared threshold parsing/formatting into alert-thresholds.ts so
  both alert types use the same USD<->microdollars contract.
recipients,
configurationVersion: alert.configuration_version,
thresholdMicrodollars,
measuredValueMicrodollars: newBalanceMicrodollars,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Negative org balances cannot be claimed, so a crossing that lands below zero never emails

claimAlertDeliveries inserts measuredValueMicrodollars: newBalanceMicrodollars, but organization_alert_deliveries enforces measured_value_microdollars >= 0. Org balances are allowed to go negative. A single debit from at/above the threshold to below zero is a real crossing (previous >= threshold && new < threshold) and then the INSERT raises check_violation. There is no per-alert try/catch, so the throw aborts the rest of this org's alerts, no delivery row is written, and the dispatch sweep cannot retry.

Clamp to Math.max(0, newBalanceMicrodollars) at claim time, or relax the check to allow negative measured values for low_balance.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

<SelectTrigger id={SCOPE_ID} className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Measure dropdown renders behind the alerts drawer

SelectContent defaults to z-50 and portals to document. The drawer overlay is z-[60] and the panel is z-index: 62. The type picker in AlertEditorPanel already sets z-[70] for this. Without it, the new Measure options are not clickable.

Suggested change
<SelectContent>
<SelectContent className="z-[70]">

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

>
<SelectValue placeholder="Choose a group" />
</SelectTrigger>
<SelectContent>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Group picker renders behind the alerts drawer

Same stacking issue as the Measure select: default z-50 is below the drawer (z-[60]/62). Group-scoped monthly spending cannot be chosen in the UI.

Suggested change
<SelectContent>
<SelectContent className="z-[70]">

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant