From e085457d0ae2e31c8253319561c872a87b455af9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 06:13:49 -0500 Subject: [PATCH] feat(#45): raise delivered-issue rate to $5.00 (rate_minor 500) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner pricing decision 2026-09-01: gold and standard now default to rate_minor 500 ($5.00) per delivered issue; free stays 0. Defaults only — SAAS_TIERS_JSON overrides still win, auto_merge untouched. Also align the SAAS_RATE_MINOR fallback (DEFAULT_RATE_MINOR) to 500 so the per-delivery default matches the new rate everywhere. Tests updated to the literal 500/500/0 values and the $5.00 debits; idempotent single-debit + replay-no-double-charge coverage already existed (deliver::: key) and now asserts 500. --- gitea_saas/config.py | 13 +++++++------ gitea_saas/delivery.py | 6 +++--- tests/test_delivery.py | 22 +++++++++++----------- tests/test_review_policy.py | 13 +++++++++++-- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/gitea_saas/config.py b/gitea_saas/config.py index 1a48c53..686d4ca 100644 --- a/gitea_saas/config.py +++ b/gitea_saas/config.py @@ -39,13 +39,14 @@ DEFAULT_DB_PATH: Optional[Path] = None # second copy of the literal. from .auth import API_KEY_HEADER -# Tier defaults (issue #6): GOLD auto-merges on a green APPROVE at rate 100 -# minor units per delivery; STANDARD is reviewed but operator-merged at rate -# 25; FREE is never auto-merged and never debited. Rates are config, not -# code — SAAS_TIERS_JSON overrides merge over these defaults. +# Tier defaults (issue #6, pricing 2026-09-01): GOLD auto-merges on a green +# APPROVE at rate 500 minor units ($5.00) per delivery; STANDARD is reviewed +# but operator-merged at rate 500 ($5.00); FREE is never auto-merged and +# never debited. Rates are config, not code — SAAS_TIERS_JSON overrides +# merge over these defaults. DEFAULT_TIERS = { - "gold": {"auto_merge": True, "rate_minor": 100}, - "standard": {"auto_merge": False, "rate_minor": 25}, + "gold": {"auto_merge": True, "rate_minor": 500}, + "standard": {"auto_merge": False, "rate_minor": 500}, "free": {"auto_merge": False, "rate_minor": 0}, } diff --git a/gitea_saas/delivery.py b/gitea_saas/delivery.py index e50972d..380a6c7 100644 --- a/gitea_saas/delivery.py +++ b/gitea_saas/delivery.py @@ -38,11 +38,11 @@ from .auth import UNAUTHORIZED_BODY, key_is_valid # --- defaults (overridable via env, mirror issue #6's config.py) ---------- DEFAULT_TIERS: dict[str, dict[str, Any]] = { - "gold": {"rate_minor": 100}, # $1.00 / issue - "standard": {"rate_minor": 25}, # $0.25 / issue + "gold": {"rate_minor": 500}, # $5.00 / issue + "standard": {"rate_minor": 500}, # $5.00 / issue "free": {"rate_minor": 0}, # never debited } -DEFAULT_RATE_MINOR = 100 # SAAS_RATE_MINOR (100 = $1.00 / issue) +DEFAULT_RATE_MINOR = 500 # SAAS_RATE_MINOR (500 = $5.00 / issue) DEFAULT_FLOOR_MINOR = 500 # SAAS_DEBIT_FLOOR_MINOR ($5.00; matches issue #4) KEY_PREFIX = "deliver" diff --git a/tests/test_delivery.py b/tests/test_delivery.py index 77834ff..d06062c 100644 --- a/tests/test_delivery.py +++ b/tests/test_delivery.py @@ -76,7 +76,7 @@ def make_delivery(core, gitea=None, intake=None, config=None): def test_merge_webhook_one_debit_at_tier_rate_with_receipt(core): gitea = FakeGitea() intake = FakeIntake() - client_id = make_client(core, tier="gold") # gold rate = 100 ($1.00) + client_id = make_client(core, tier="gold") # gold rate = 500 ($5.00) credit(core, client_id, 10000) # $100.00 d = make_delivery(core, gitea=gitea, intake=intake) @@ -84,13 +84,13 @@ def test_merge_webhook_one_debit_at_tier_rate_with_receipt(core): assert result["ok"] is True assert result["overdue"] is False - assert result["rate_minor"] == 100 - assert result["balance"] == 9900 # 10000 - 100 + assert result["rate_minor"] == 500 + assert result["balance"] == 9500 # 10000 - 500 assert result["replayed"] is False # exactly one receipt comment, mentioning balance + delivered check assert len(gitea.comments) == 1 assert "Delivered \u2713" in gitea.comments[0]["body"] - assert "$99.00" in gitea.comments[0]["body"] + assert "$95.00" in gitea.comments[0]["body"] # debit recorded in ledger with the delivery idempotency key + pr ref ok, ledger = core.list_ledger(client_id) assert ok @@ -120,7 +120,7 @@ def test_duplicate_webhook_no_second_debit_no_second_comment(core): assert first["ok"] is True assert second["ok"] is True assert second["replayed"] is True - assert second["balance"] == first["balance"] == 9900 + assert second["balance"] == first["balance"] == 9500 # exactly one debit row, one comment ok, ledger = core.list_ledger(client_id) assert ok @@ -138,7 +138,7 @@ def test_balance_below_floor_locks_client(core): gitea = FakeGitea() intake = FakeIntake() client_id = make_client(core, tier="gold") - credit(core, client_id, 550) # $5.50; after $1.00 debit -> $4.50 < $5.00 floor + credit(core, client_id, 550) # $5.50; after $5.00 debit -> $0.50 < $5.00 floor d = make_delivery(core, gitea=gitea, intake=intake) result = d.on_merge_webhook(client_id, REPO, PR, HEAD) @@ -147,7 +147,7 @@ def test_balance_below_floor_locks_client(core): assert result["below_floor"] is True assert result["locked"] is True assert intake.locked == [client_id] - assert result["balance"] == 450 + assert result["balance"] == 50 assert len(gitea.comments) == 1 @@ -159,7 +159,7 @@ def test_negative_debit_does_not_block_merge_overdue_and_lock(core): gitea = FakeGitea() intake = FakeIntake() client_id = make_client(core, tier="gold") - credit(core, client_id, 50) # $0.50 < $1.00 debit -> would go negative + credit(core, client_id, 50) # $0.50 < $5.00 debit -> would go negative d = make_delivery(core, gitea=gitea, intake=intake) result = d.on_merge_webhook(client_id, REPO, PR, HEAD) @@ -214,7 +214,7 @@ def test_webhook_with_auth_ok(core): result, status = d.handle_merge_webhook(payload, provided_key="sekret") assert status == 200 assert result["ok"] is True - assert result["balance"] == 9900 + assert result["balance"] == 9500 assert len(gitea.comments) == 1 @@ -247,7 +247,7 @@ def test_webhook_tolerant_gitea_shape(core): result, status = d.handle_merge_webhook(payload, provided_key="sekret") assert status == 200 assert result["ok"] is True - assert result["balance"] == 9900 + assert result["balance"] == 9500 assert len(gitea.comments) == 1 @@ -257,7 +257,7 @@ def test_webhook_tolerant_gitea_shape(core): def test_rate_from_tier_config_override(core): gitea = FakeGitea() - client_id = make_client(core, tier="standard") # standard default 25 + client_id = make_client(core, tier="standard") # standard default 500 credit(core, client_id, 10000) config = default_config({"SAAS_TIERS_JSON": '{"standard": {"rate_minor": 77}}'}) d = make_delivery(core, gitea=gitea, config=config) diff --git a/tests/test_review_policy.py b/tests/test_review_policy.py index a4d73ee..d068a5f 100644 --- a/tests/test_review_policy.py +++ b/tests/test_review_policy.py @@ -315,14 +315,14 @@ def test_gold_approve_merges_closes_strips_debits_once(policy_factory): # One debit with the idempotent key repo#pr@head, at the gold rate. # (The fixture pre-funds the client, so filter debit rows only.) ok, balances = core.get_balance(c["id"]) - assert ok and balances["balance"] == 10_000 - 100 # gold rate_minor from config + assert ok and balances["balance"] == 10_000 - 500 # gold rate_minor from config ok, ledger = core.list_ledger(c["id"]) assert ok debits = [e for e in ledger["entries"] if e["kind"] == "debit"] assert len(debits) == 1 entry = debits[0] assert entry["idempotency_key"] == "gitea-saas-core#42@sha-gold" - assert entry["amount_minor"] == 100 and entry["kind"] == "debit" + assert entry["amount_minor"] == 500 and entry["kind"] == "debit" def test_gold_double_delivery_never_double_debits(policy_factory): @@ -471,6 +471,15 @@ def test_load_review_config_merges_saas_tiers_json(monkeypatch): assert config["test_timeout_s"] == 45 +def test_default_tier_rates_are_500_500_0(): + # Pricing decision 2026-09-01: delivered-issue rate is $5.00 (500 minor + # units) for both gold and standard; free stays 0. Assert the literal + # values, not the constant name (issue #45 acceptance). + assert DEFAULT_TIERS["gold"]["rate_minor"] == 500 + assert DEFAULT_TIERS["standard"]["rate_minor"] == 500 + assert DEFAULT_TIERS["free"]["rate_minor"] == 0 + + # --------------------------------------------------------------------------- # Linked issue parsing + webhook path # --------------------------------------------------------------------------- -- 2.43.0