95 lines
2.9 KiB
Python
95 lines
2.9 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}")
|
|
|
|
|
|
# Largest single payment between participants (whole credits). No cash-out, ever.
|
|
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}"
|