1"""Governance relay ledger service: quota credit-in operations.
2
3Journals top-up and check-in credits through the shared ledger store —
4the framework records usage but no wallet balance, so credits are
5journaled records applications and operators can audit and settle on.
6Check-in awards are caller-supplied: amounts and cadences are
7application policy, not framework constants. Every mutation emits a
8structured event and returns a domain error value (never raises).
9"""
10
11from __future__ import annotations
12
13from decimal import Decimal, InvalidOperation
14from typing import TYPE_CHECKING
15
16from lexigram.contracts.ai.relay import (
17 RelayCheckinRecord,
18 RelayLedgerError,
19 RelayLedgerServiceProtocol,
20 RelayTopUpRecord,
21)
22from lexigram.identity import ambient as identity
23from lexigram.logging import get_logger
24from lexigram.primitives import clock
25from lexigram.result import Err, Ok, Result
26
27if TYPE_CHECKING:
28 from lexigram.ai.governance.relay_ledger.persistence import (
29 SqlRelayLedgerStore,
30 )
31 from lexigram.contracts.ai.governance import RelayUsageScope
32
33logger = get_logger(__name__)
34
35__all__ = ["RelayLedgerService"]
36
37
38def _validate_amount(amount: str) -> bool:
39 """Return whether *amount* is a finite, non-negative decimal."""
40 try:
41 return Decimal(amount) >= 0
42 except (InvalidOperation, ValueError):
43 return False
44
45
46class RelayLedgerService(RelayLedgerServiceProtocol):
47 """Credit-in operations over the ledger store.
48
49 Args:
50 store: SQL ledger store; the only persistence boundary used.
51 """
52
53 def __init__(self, store: SqlRelayLedgerStore) -> None:
54 self._store = store
55
56 async def credit(
57 self, scope: RelayUsageScope, amount: str, reason: str
58 ) -> Result[None, RelayLedgerError]:
59 """Journal an immediate completed credit for *scope*.
60
61 Args:
62 scope: Accounting scope the credit applies to; the user is
63 required.
64 amount: Credited amount as a Decimal string.
65 reason: Human-readable reason recorded with the journal row.
66
67 Returns:
68 ``Ok(None)`` when the credit was journaled, ``Err`` with
69 ``invalid_amount`` or ``invalid_scope`` otherwise.
70 """
71 if scope.user_id is None:
72 return Err(
73 RelayLedgerError(
74 code="invalid_scope",
75 message="credit requires a user scope",
76 )
77 )
78 if not _validate_amount(amount):
79 return Err(
80 RelayLedgerError(
81 code="invalid_amount",
82 message="amount must be a non-negative decimal",
83 )
84 )
85 record = RelayTopUpRecord(
86 reference_id=f"credit-{identity.new_uuid()}",
87 user_id=scope.user_id,
88 amount=amount,
89 status="completed",
90 created_at=clock.now().isoformat(),
91 )
92 await self._store.insert_topup(record)
93 logger.info(
94 "relay_ledger.credit",
95 user_id=record.user_id,
96 amount=amount,
97 reason=reason,
98 )
99 return Ok(None)
100
101 async def settle_topup(
102 self, reference_id: str, expected_status: str
103 ) -> Result[None, RelayLedgerError]:
104 """Flip *reference_id* from *expected_status* to completed exactly once.
105
106 Args:
107 reference_id: Top-up reference to settle.
108 expected_status: Status the caller observed; only matching
109 rows are flipped.
110
111 Returns:
112 ``Ok(None)`` on the single successful settle, ``Err`` with
113 ``not_found`` or ``stale_settlement`` otherwise.
114 """
115 flipped = await self._store.settle_topup(reference_id, expected_status)
116 if not flipped:
117 current = await self._store.topup_status(reference_id)
118 if current is None:
119 return Err(
120 RelayLedgerError(
121 code="not_found",
122 message=f"top-up {reference_id!r} does not exist",
123 )
124 )
125 return Err(
126 RelayLedgerError(
127 code="stale_settlement",
128 message=(
129 f"top-up {reference_id!r} already settled (status {current!r})"
130 ),
131 )
132 )
133 logger.info(
134 "relay_ledger.settle_topup",
135 reference_id=reference_id,
136 expected_status=expected_status,
137 )
138 return Ok(None)
139
140 async def checkin(
141 self, user_id: str, award: str
142 ) -> Result[RelayCheckinRecord, RelayLedgerError]:
143 """Award *award* to *user_id* once per UTC day.
144
145 Args:
146 user_id: User receiving the award.
147 award: Awarded amount as a Decimal string, supplied by the
148 caller (application policy).
149
150 Returns:
151 ``Ok(record)`` when the award was written, ``Err`` with
152 ``already_checked_in`` or ``invalid_amount`` otherwise.
153 """
154 if not _validate_amount(award):
155 return Err(
156 RelayLedgerError(
157 code="invalid_amount",
158 message="award must be a non-negative decimal",
159 )
160 )
161 record = RelayCheckinRecord(
162 user_id=user_id,
163 day=clock.now().date().isoformat(),
164 award=award,
165 created_at=clock.now().isoformat(),
166 )
167 written = await self._store.insert_checkin(record)
168 if not written:
169 return Err(
170 RelayLedgerError(
171 code="already_checked_in",
172 message=f"user {user_id!r} already checked in today",
173 )
174 )
175 logger.info(
176 "relay_ledger.checkin",
177 user_id=record.user_id,
178 day=record.day,
179 award=record.award,
180 )
181 return Ok(record)
182
183 async def list_topups(
184 self, user_id: str | None, limit: int
185 ) -> list[RelayTopUpRecord]:
186 """List top-up records, newest first, optionally for one user.
187
188 Args:
189 user_id: When set, only this user's records are listed.
190 limit: Maximum number of records to return.
191
192 Returns:
193 The matching records, newest first.
194 """
195 return await self._store.list_topups(user_id, limit)