Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions products/billing/frontend/generated/api.schemas.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 0 additions & 69 deletions products/billing/frontend/generated/api.zod.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion products/billing_alerts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@ The MCP server exposes atomic create and partial-update tools for billing alert

Billing alerts evaluate one UTC billing date every 24 hours. Alert configuration and destination changes are committed atomically through the generated billing API, while the shared alerting backend owns destination persistence and delivery.

The Billing tab uses the shared alert editor, destination editor, advanced options, next-evaluation status, and evaluation-history chart from `products/alerts`. Billing supplies its own form, threshold labels, value formatting, API calls, and notification destination payloads.
The Billing tab uses the shared alert editor, destination editor, next-evaluation status, and evaluation-history chart from `products/alerts`. Billing supplies its own form, threshold labels, value formatting, API calls, and notification destination payloads.
2 changes: 1 addition & 1 deletion products/billing_alerts/backend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def validate_threshold_configuration(
threshold_type: str,
threshold_percentage: Decimal | None,
threshold_value: Decimal | None,
minimum_value: Decimal,
minimum_value: Decimal = Decimal("0"),
) -> dict[str, str]:
"""Threshold policy shared by model validation and the DRF serializer."""
errors: dict[str, str] = {}
Expand Down
27 changes: 0 additions & 27 deletions products/billing_alerts/backend/presentation/serializers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

from decimal import Decimal
from typing import Any, cast
from urllib.parse import urlparse

Expand Down Expand Up @@ -205,30 +204,12 @@ class BillingAlertConfigurationSerializer(serializers.ModelSerializer):
allow_null=True,
help_text="Absolute value or absolute increase that triggers absolute threshold alerts.",
)
minimum_value = serializers.DecimalField(
max_digits=20,
decimal_places=6,
required=False,
help_text="Minimum current value before the alert can fire.",
)
baseline_window_days = serializers.IntegerField(
required=False,
min_value=1,
max_value=90,
help_text="Reserved for future increase-over-baseline rules. Not used by absolute value alerts.",
)
evaluation_delay_hours = serializers.IntegerField(
required=False,
min_value=0,
max_value=72,
help_text="Hours after a UTC billing date ends before it becomes eligible for evaluation.",
)
cooldown_hours = serializers.IntegerField(
required=False,
min_value=0,
max_value=24 * 30,
help_text="Minimum hours between repeated firing notifications.",
)
snoozed_until = serializers.DateTimeField(
required=False,
allow_null=True,
Expand Down Expand Up @@ -260,11 +241,8 @@ class Meta:
"threshold_type",
"threshold_percentage",
"threshold_value",
"minimum_value",
"baseline_window_days",
"evaluation_delay_hours",
"state",
"cooldown_hours",
"snoozed_until",
"next_check_at",
"last_checked_at",
Expand Down Expand Up @@ -334,13 +312,11 @@ def validate(self, attrs: dict[str, Any]) -> dict[str, Any]:
current.threshold_percentage if current else None,
)
threshold_value = attrs.get("threshold_value", current.threshold_value if current else None)
minimum_value = attrs.get("minimum_value", current.minimum_value if current else Decimal("0"))

errors = validate_threshold_configuration(
threshold_type=threshold_type,
threshold_percentage=threshold_percentage,
threshold_value=threshold_value,
minimum_value=minimum_value,
)
if errors:
raise ValidationError(errors)
Expand Down Expand Up @@ -382,14 +358,11 @@ def update(
"threshold_type",
"threshold_percentage",
"threshold_value",
"minimum_value",
"baseline_window_days",
"evaluation_delay_hours",
}
revision_fields = evaluation_fields | {
"enabled",
"snoozed_until",
"cooldown_hours",
}
with transaction.atomic():
locked = BillingAlertConfiguration.objects.select_for_update().get(
Expand Down
23 changes: 13 additions & 10 deletions products/billing_alerts/backend/test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,6 @@ def _payload(self, **overrides) -> dict:
"metric": "spend",
"threshold_type": "absolute_value",
"threshold_value": "100.00",
"minimum_value": "0",
"evaluation_delay_hours": 6,
"cooldown_hours": 24,
}
payload.update(overrides)
return payload
Expand Down Expand Up @@ -234,7 +231,7 @@ def test_copy_only_edit_does_not_bump_configuration_revision(self) -> None:
def test_evaluation_rule_edits_reset_firing_state_and_failures(self) -> None:
updates = [
("baseline_window_days", 14),
("evaluation_delay_hours", 12),
("threshold_value", Decimal("150.000000")),
]

for field, value in updates:
Expand Down Expand Up @@ -262,13 +259,13 @@ def test_threshold_edit_preserves_snoozed_state(self) -> None:

response = self.client.patch(
f"{self.url}{alert.id}/",
{"minimum_value": "10.00"},
{"threshold_value": "150.00"},
format="json",
)

assert response.status_code == status.HTTP_200_OK, response.json()
alert.refresh_from_db()
assert alert.minimum_value == Decimal("10.00")
assert alert.threshold_value == Decimal("150.00")
assert alert.state == BillingAlertConfiguration.State.SNOOZED
assert alert.snoozed_until == snoozed_until
assert alert.consecutive_failures == 3
Expand Down Expand Up @@ -411,11 +408,17 @@ def test_teams_destinations_require_a_microsoft_workflow_webhook(self) -> None:
assert "webhook_url" in invalid.errors
assert valid.is_valid(), valid.errors

def test_rejects_negative_minimum(self) -> None:
negative_minimum = self.client.post(self.url, self._payload(minimum_value="-1"), format="json")
def test_fields_the_platform_ignores_are_not_writable(self) -> None:
response = self.client.post(
self.url,
self._payload(minimum_value="200", evaluation_delay_hours=24, cooldown_hours=48),
format="json",
)

assert negative_minimum.status_code == status.HTTP_400_BAD_REQUEST
assert negative_minimum.json()["attr"] == "minimum_value"
assert response.status_code == status.HTTP_201_CREATED, response.json()
assert not {"minimum_value", "evaluation_delay_hours", "cooldown_hours"} & response.json().keys()
alert = BillingAlertConfiguration.objects.get(id=response.json()["id"])
assert (alert.minimum_value, alert.evaluation_delay_hours, alert.cooldown_hours) == (Decimal("0"), 6, 24)
Comment thread
jzhu13 marked this conversation as resolved.

def test_check_now_uses_shared_organization_object_permissions(self) -> None:
alert = BillingAlertConfiguration.objects.create(
Expand Down
Loading
Loading