@@ -10,7 +10,14 @@ import { create } from 'zustand';
1010import { persist , createJSONStorage } from 'zustand/middleware' ;
1111import { asyncStorageAdapter } from '../../src/utils/storage' ;
1212
13- export type CreditTxKind = 'issue' | 'apply' | 'transfer_in' | 'transfer_out' | 'expire' ;
13+ export type CreditTxKind =
14+ | 'issue'
15+ | 'apply'
16+ | 'transfer_in'
17+ | 'transfer_out'
18+ | 'expire'
19+ | 'deposit'
20+ | 'withdraw' ;
1421
1522export type ExpirationPolicy = { kind : 'never' } | { kind : 'after_secs' ; seconds : number } ;
1623
@@ -46,6 +53,26 @@ export interface CreditApplied {
4653 balanceAfter : number ;
4754}
4855
56+ /**
57+ * Consolidated account-balance summary for a subscriber, combining on-book
58+ * credit (available/issued/expired/applied/transferred) with any prepayment
59+ * wallet funds. Used for high-level balance display and reconciliation.
60+ */
61+ export interface AccountBalance {
62+ subscriber : string ;
63+ availableCredit : number ;
64+ totalIssued : number ;
65+ totalApplied : number ;
66+ totalExpired : number ;
67+ totalTransferredIn : number ;
68+ totalTransferredOut : number ;
69+ totalDeposited : number ;
70+ totalWithdrawn : number ;
71+ prepaymentBalance : number ;
72+ netBalance : number ;
73+ nextExpirationAt ?: number ;
74+ }
75+
4976const isExpired = ( lot : CreditLot , now : number ) : boolean =>
5077 lot . expiresAt !== undefined && lot . expiresAt <= now ;
5178
@@ -57,16 +84,31 @@ const availableOf = (account: AccountCredit, now: number): number =>
5784
5885interface CreditStoreState {
5986 accounts : Record < string , AccountCredit > ;
87+ wallets : Record < string , CreditWallet > ;
6088 nextId : number ;
6189 now : ( ) => number ;
6290
6391 issueCredit : ( subscriber : string , amount : number , reason : string , expiresAt ?: number ) => void ;
6492 setExpirationPolicy : ( subscriber : string , policy : ExpirationPolicy ) => void ;
6593 applyCredit : ( subscriber : string , subscriptionId : string , amountDue : number ) => CreditApplied ;
6694 transferCredit : ( from : string , to : string , amount : number , reason : string ) => boolean ;
95+ depositCredit : ( subscriber : string , amount : number , reason : string ) => void ;
96+ withdrawCredit : ( subscriber : string , amount : number , reason : string ) => boolean ;
6797 expireCredits : ( subscriber : string ) => number ;
6898 getBalance : ( subscriber : string ) => number ;
6999 getAccount : ( subscriber : string ) => AccountCredit ;
100+ getAccountBalance : ( subscriber : string ) => AccountBalance ;
101+ getAccountBalances : ( ) => AccountBalance [ ] ;
102+ }
103+
104+ /** Prepayment wallet tracked alongside credit accounts. */
105+ export interface CreditWallet {
106+ id : string ;
107+ subscriber : string ;
108+ currency : string ;
109+ balance : number ;
110+ totalDeposited : number ;
111+ totalWithdrawn : number ;
70112}
71113
72114const blankAccount = ( subscriber : string ) : AccountCredit => ( {
@@ -141,6 +183,7 @@ export const useCreditStore = create<CreditStoreState>()(
141183
142184 return {
143185 accounts : { } ,
186+ wallets : { } ,
144187 nextId : 0 ,
145188 now : ( ) => Math . floor ( Date . now ( ) / 1000 ) ,
146189
@@ -217,13 +260,89 @@ export const useCreditStore = create<CreditStoreState>()(
217260
218261 getBalance : ( subscriber ) => availableOf ( account ( subscriber ) , get ( ) . now ( ) ) ,
219262 getAccount : ( subscriber ) => account ( subscriber ) ,
263+
264+ depositCredit : ( subscriber , amount , reason ) => {
265+ if ( amount <= 0 ) return ;
266+ const now = get ( ) . now ( ) ;
267+ const acc = cloneAccount ( account ( subscriber ) ) ;
268+ realizeExpiry ( acc , now ) ;
269+ acc . balance += amount ;
270+ acc . lots . push ( { id : nextId ( ) , remaining : amount , issuedAt : now } ) ;
271+ record ( acc , 'deposit' , amount , reason ) ;
272+ commit ( acc ) ;
273+ } ,
274+
275+ withdrawCredit : ( subscriber , amount , reason ) => {
276+ if ( amount <= 0 ) return false ;
277+ const now = get ( ) . now ( ) ;
278+ const acc = cloneAccount ( account ( subscriber ) ) ;
279+ realizeExpiry ( acc , now ) ;
280+ if ( availableOf ( acc , now ) < amount ) return false ;
281+ const moved = consume ( acc , now , amount ) ;
282+ acc . balance -= moved ;
283+ record ( acc , 'withdraw' , - moved , reason ) ;
284+ commit ( acc ) ;
285+ return true ;
286+ } ,
287+
288+ getAccountBalance : ( subscriber ) : AccountBalance => {
289+ const now = get ( ) . now ( ) ;
290+ const acc = account ( subscriber ) ;
291+ const availableCredit = availableOf ( acc , now ) ;
292+ const totalApplied = acc . transactions
293+ . filter ( ( t ) => t . kind === 'apply' )
294+ . reduce ( ( sum , t ) => sum + Math . abs ( t . amount ) , 0 ) ;
295+ const totalExpired = acc . transactions
296+ . filter ( ( t ) => t . kind === 'expire' )
297+ . reduce ( ( sum , t ) => sum + Math . abs ( t . amount ) , 0 ) ;
298+ const totalTransferredIn = acc . transactions
299+ . filter ( ( t ) => t . kind === 'transfer_in' )
300+ . reduce ( ( sum , t ) => sum + Math . abs ( t . amount ) , 0 ) ;
301+ const totalTransferredOut = acc . transactions
302+ . filter ( ( t ) => t . kind === 'transfer_out' )
303+ . reduce ( ( sum , t ) => sum + Math . abs ( t . amount ) , 0 ) ;
304+ const totalIssued = acc . transactions
305+ . filter ( ( t ) => t . kind === 'issue' || t . kind === 'deposit' )
306+ . reduce ( ( sum , t ) => sum + Math . abs ( t . amount ) , 0 ) ;
307+
308+ const wallets = Object . values ( get ( ) . wallets ) . filter (
309+ ( w ) => w . subscriber === subscriber
310+ ) ;
311+ const totalDeposited = wallets . reduce ( ( s , w ) => s + w . totalDeposited , 0 ) ;
312+ const totalWithdrawn = wallets . reduce ( ( s , w ) => s + w . totalWithdrawn , 0 ) ;
313+ const prepaymentBalance = wallets . reduce ( ( s , w ) => s + w . balance , 0 ) ;
314+
315+ const expiringLots = acc . lots
316+ . filter ( ( lot ) => lot . remaining > 0 && lot . expiresAt !== undefined && lot . expiresAt > now )
317+ . sort ( ( a , b ) => ( a . expiresAt ?? 0 ) - ( b . expiresAt ?? 0 ) ) ;
318+
319+ return {
320+ subscriber,
321+ availableCredit,
322+ totalIssued,
323+ totalApplied,
324+ totalExpired,
325+ totalTransferredIn,
326+ totalTransferredOut,
327+ totalDeposited,
328+ totalWithdrawn,
329+ prepaymentBalance,
330+ netBalance : availableCredit + prepaymentBalance ,
331+ nextExpirationAt : expiringLots [ 0 ] ?. expiresAt ,
332+ } ;
333+ } ,
334+
335+ getAccountBalances : ( ) =>
336+ [ ...new Set ( [ ...Object . keys ( get ( ) . accounts ) , ...Object . keys ( get ( ) . wallets ) ] ) ]
337+ . map ( ( sub ) => get ( ) . getAccountBalance ( sub ) ) ,
220338 } ;
221339 } ,
222340 {
223341 name : 'subtrackr-credit-store' ,
224342 storage : createJSONStorage ( ( ) => asyncStorageAdapter ) ,
225343 partialize : ( state ) => ( {
226344 accounts : state . accounts ,
345+ wallets : state . wallets ,
227346 nextId : state . nextId ,
228347 } ) ,
229348 }
0 commit comments