coffer/coffer/units.py

96 lines
3.0 KiB
Python

"""Credit units.
One agent credit equals one US dollar. Catalog prices, grants, and top-ups are
whole credits. The ledger stores **centicredits** (1 credit = 100 centicredits)
so reseller settlement can record Coffer's 1% cut (0.2c on a 20c seat).
"""
SCALE = 100 # centicredits per credit
SIGNUP_GRANT_CREDITS = 40
SUBSCRIPTION_CREDITS = 20
SHIRT_CREDITS = 20
CONTRACT_CREDITS = 200
COFFER_INVESTMENT_CREDITS = 500
LEORA_FUNDED_AFTER_GRANTS_CREDITS = 400
# Reseller cut on subscription seats billed through Coffer.
SUBSCRIPTION_CUT_BPS = 100 # 1% = 100 basis points
def to_subunits(credits: int) -> int:
if type(credits) is not int:
raise ValueError("credits must be a whole number")
if credits < 0:
raise ValueError("credits must be non-negative")
return credits * SCALE
def from_subunits(subunits: int) -> float:
return subunits / SCALE
def format_credits(subunits: int) -> str:
whole, frac = divmod(abs(subunits), SCALE)
sign = "-" if subunits < 0 else ""
if frac:
return f"{sign}{whole}.{frac:02d}"
return f"{sign}{whole}"
def cut_subunits(gross_subunits: int, bps: int = SUBSCRIPTION_CUT_BPS) -> int:
"""Integer cut in centicredits from basis points (100 bps = 1%)."""
if type(gross_subunits) is not int or gross_subunits < 0:
raise ValueError("gross must be a non-negative whole number of subunits")
if type(bps) is not int or bps < 0:
raise ValueError("bps must be a non-negative integer")
return (gross_subunits * bps) // 10_000
# Well-known account ids (company books stay separate from founder agents).
ACCOUNT_COFFER = "company:coffer"
ACCOUNT_NEW_INTERNET = "company:new-internet-co"
ACCOUNT_LEORA = "agent:leora"
ACCOUNT_SIGIL = "company:sigil"
def agent_account(agent_id: str) -> str:
aid = (agent_id or "").strip()
if not aid:
raise ValueError("agent_id is required")
if aid.startswith(("agent:", "company:", "human:")):
return aid
return f"agent:{aid}"
def human_account(handle: str) -> str:
"""Wallet id for a person (a Gitea account linked to messaging). Public by handle only."""
h = (handle or "").strip()
if h.startswith("human:"):
h = h[len("human:"):]
if not h or not all(c.isalnum() or c in "-_." for c in h) or len(h) > 40:
raise ValueError("a human handle is required")
return f"human:{h}"
def account_kind(account_id: str) -> str:
"""'agent', 'human' or 'company' from an account id."""
prefix = (account_id or "").split(":", 1)[0]
if prefix in {"agent", "human", "company"}:
return prefix
raise ValueError(f"unknown account id: {account_id!r}")
# Credit packs a person can buy (prepaid, whole credits). No cash-out, ever.
CREDIT_PACKS = (10, 50, 100, 500)
MAX_PAYMENT_CREDITS = 1000
def company_account(company_id: str) -> str:
cid = (company_id or "").strip()
if not cid:
raise ValueError("company_id is required")
if cid.startswith("company:"):
return cid
return f"company:{cid}"