feat(#45): Pricing: raise delivered-issue rate to $5.00 (rate_minor 500) #46
@ -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},
|
||||
}
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Loading…
Reference in New Issue
Block a user