Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-governance/src/lexigram/ai/governance/relay_ledger/ledger.py: 36%

47 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 07:19 +0800

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)