From a413539f6ac2185b214122e4c4eed5d20d1f661d Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Wed, 1 Jul 2026 17:08:58 +0200 Subject: [PATCH] feat(capabilities): allow free verbs with no billing unit --- surfsense_backend/app/capabilities/billing.py | 6 ++++-- surfsense_backend/app/capabilities/types.py | 4 ++-- .../tests/unit/capabilities/test_billing.py | 11 +++++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/surfsense_backend/app/capabilities/billing.py b/surfsense_backend/app/capabilities/billing.py index 3086c09bd..ed1b00e13 100644 --- a/surfsense_backend/app/capabilities/billing.py +++ b/surfsense_backend/app/capabilities/billing.py @@ -17,9 +17,11 @@ if TYPE_CHECKING: async def charge_capability( - output: BillableOutput, unit: BillingUnit, ctx: CapabilityContext + output: BillableOutput, unit: BillingUnit | None, ctx: CapabilityContext ) -> None: - """Bill the workspace owner for this result's billable successes (03c).""" + """Bill the workspace owner for this result's billable successes (03c). ``None`` = free.""" + if unit is None: + return units = output.billable_units if units <= 0: return diff --git a/surfsense_backend/app/capabilities/types.py b/surfsense_backend/app/capabilities/types.py index 0a9c261e8..6cb69c312 100644 --- a/surfsense_backend/app/capabilities/types.py +++ b/surfsense_backend/app/capabilities/types.py @@ -13,7 +13,7 @@ if TYPE_CHECKING: class BillingUnit(StrEnum): - """The meter a verb charges on (priced by the billing service, 03c).""" + """The meter a verb charges on (priced by the billing service, 03c). ``None`` = free.""" WEB_CRAWL = "web_crawl" @@ -44,4 +44,4 @@ class Capability: input_schema: type[BaseModel] output_schema: type[BaseModel] executor: Executor - billing_unit: BillingUnit + billing_unit: BillingUnit | None diff --git a/surfsense_backend/tests/unit/capabilities/test_billing.py b/surfsense_backend/tests/unit/capabilities/test_billing.py index 53600c9c7..210e69784 100644 --- a/surfsense_backend/tests/unit/capabilities/test_billing.py +++ b/surfsense_backend/tests/unit/capabilities/test_billing.py @@ -114,3 +114,14 @@ async def test_disabled_is_noop(monkeypatch, record_usage): record_usage.assert_not_awaited() session.execute.assert_not_called() assert user.credit_micros_balance == 100_000 + + +async def test_free_verb_without_a_unit_is_noop(monkeypatch, record_usage): + monkeypatch.setattr(config, "WEB_CRAWL_CREDIT_BILLING_ENABLED", True) + session, user = _make_session(_OWNER, balance_micros=100_000) + + await charge_capability(_output("success", "success"), None, _ctx(session)) + + record_usage.assert_not_awaited() + session.execute.assert_not_called() + assert user.credit_micros_balance == 100_000