@drawbridge/drawbridge-utils 0.0.80 → 0.0.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ai.cjs CHANGED
@@ -101,9 +101,6 @@ var insertTransaction = async ({
101
101
  session,
102
102
  ...rest
103
103
  }) => {
104
- var _a;
105
- const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
106
- const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
107
104
  const trace = (0, import_drawbridge_telemetry.currentTraceId)();
108
105
  await db.create({
109
106
  authenticated: user,
@@ -115,7 +112,6 @@ var insertTransaction = async ({
115
112
  category,
116
113
  source,
117
114
  amount,
118
- ...balance && { balance },
119
115
  ...trace && { trace },
120
116
  ...rest,
121
117
  ...stripeInvoiceId && { stripeInvoiceId },
package/dist/ai.js CHANGED
@@ -67,9 +67,6 @@ var insertTransaction = async ({
67
67
  session,
68
68
  ...rest
69
69
  }) => {
70
- var _a;
71
- const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
72
- const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
73
70
  const trace = currentTraceId();
74
71
  await db.create({
75
72
  authenticated: user,
@@ -81,7 +78,6 @@ var insertTransaction = async ({
81
78
  category,
82
79
  source,
83
80
  amount,
84
- ...balance && { balance },
85
81
  ...trace && { trace },
86
82
  ...rest,
87
83
  ...stripeInvoiceId && { stripeInvoiceId },
package/dist/billing.cjs CHANGED
@@ -22,6 +22,7 @@ __export(billing_exports, {
22
22
  MARKUP: () => MARKUP,
23
23
  action: () => action,
24
24
  ai: () => ai,
25
+ balance: () => balance,
25
26
  grant: () => grant,
26
27
  scrape: () => scrape
27
28
  });
@@ -42,9 +43,6 @@ var insertTransaction = async ({
42
43
  session,
43
44
  ...rest
44
45
  }) => {
45
- var _a;
46
- const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
47
- const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
48
46
  const trace = (0, import_drawbridge_telemetry.currentTraceId)();
49
47
  await db.create({
50
48
  authenticated: user,
@@ -56,7 +54,6 @@ var insertTransaction = async ({
56
54
  category,
57
55
  source,
58
56
  amount,
59
- ...balance && { balance },
60
57
  ...trace && { trace },
61
58
  ...rest,
62
59
  ...stripeInvoiceId && { stripeInvoiceId },
@@ -341,6 +338,28 @@ var scrape = {
341
338
  }
342
339
  }
343
340
  };
341
+ var balance = async ({ db, user, type }) => {
342
+ const userId = user && user.id || user;
343
+ if (!db || !userId || !type) return 0;
344
+ const [checkpoint] = await db.aggregate({
345
+ collection: "balanceCheckpoint",
346
+ pipeline: [
347
+ { $match: { user: userId, type } },
348
+ { $sort: { _id: -1 } },
349
+ { $limit: 1 }
350
+ ]
351
+ }).catch(() => []);
352
+ const match = { user: userId, type };
353
+ if (checkpoint == null ? void 0 : checkpoint.throughId) match._id = { $gt: checkpoint.throughId };
354
+ const [summed] = await db.aggregate({
355
+ collection: "transaction",
356
+ pipeline: [
357
+ { $match: match },
358
+ { $group: { _id: null, total: { $sum: "$amount" } } }
359
+ ]
360
+ });
361
+ return ((checkpoint == null ? void 0 : checkpoint.value) || 0) + ((summed == null ? void 0 : summed.total) || 0);
362
+ };
344
363
  var grant = {
345
364
  // System-issued credit (signup welcome, migration catch-up). Pass `session`
346
365
  // to enroll the credit in the caller's transaction.
@@ -471,6 +490,7 @@ var action = {
471
490
  MARKUP,
472
491
  action,
473
492
  ai,
493
+ balance,
474
494
  grant,
475
495
  scrape
476
496
  });
@@ -318,10 +318,58 @@ const scrape = {
318
318
 
319
319
  };
320
320
 
321
+ // ─── Balance (derived from the ledger) ──────────────────────────────────────────
322
+
323
+ // The AI credit balance is DERIVED, never stored — so it cannot drift from the
324
+ // ledger. balance = the newest checkpoint's cumulative value + Σ amount of the
325
+ // rows after it (credits +, debits −); with no checkpoint it sums the whole
326
+ // ledger. Checkpoints (`balanceCheckpoint`) are immutable rollup rows written by
327
+ // drawbridge-sync's nightly job to bound the scan on long-lived accounts — this
328
+ // reader tolerates their absence and falls back to the full sum. The
329
+ // { user:1, type:1, _id:1, amount:1 } index on `transaction` keeps both the full
330
+ // and post-checkpoint sums index-only.
331
+ //
332
+ // `user` accepts an id string or a user object (uses `.id`). `db` is any
333
+ // @drawbridge/mongodb controller (uses `.aggregate`). Returns integer cents;
334
+ // MAY be negative when a generation over-spent past zero — callers clamp the
335
+ // DISPLAY to 0, while the credit gate treats `<= 0` as "no credits".
336
+ const balance = async ({ db, user, type }) => {
337
+
338
+ const userId = ( user && user.id ) || user;
339
+
340
+ if( ! db || ! userId || ! type ) return 0;
341
+
342
+ // Newest immutable checkpoint: its `value` is the cumulative balance through
343
+ // `throughId`, so we only sum rows inserted after it. Missing collection / no
344
+ // checkpoint yet → [] and we sum the whole ledger.
345
+ const [ checkpoint ] = await db.aggregate({
346
+ collection : 'balanceCheckpoint',
347
+ pipeline : [
348
+ { $match : { user : userId, type } },
349
+ { $sort : { _id : -1 } },
350
+ { $limit : 1 }
351
+ ]
352
+ }).catch( () => [] );
353
+
354
+ const match = { user : userId, type };
355
+ if( checkpoint?.throughId ) match._id = { $gt : checkpoint.throughId };
356
+
357
+ const [ summed ] = await db.aggregate({
358
+ collection : 'transaction',
359
+ pipeline : [
360
+ { $match : match },
361
+ { $group : { _id : null, total : { $sum : '$amount' } } }
362
+ ]
363
+ });
364
+
365
+ return ( checkpoint?.value || 0 ) + ( summed?.total || 0 );
366
+
367
+ };
368
+
321
369
  // ─── Grants (credit rail) ──────────────────────────────────────────────────────
322
370
 
323
- // Promotional credits to a user's AI balance. `source` captures who acted; the
324
- // stream listener on the transaction collection does the balance.ai $inc.
371
+ // Promotional credits to a user's AI balance append-only rows the derived
372
+ // `balance` sums. `source` captures who acted.
325
373
  const grant = {
326
374
 
327
375
  // System-issued credit (signup welcome, migration catch-up). Pass `session`
@@ -497,4 +545,4 @@ const action = {
497
545
 
498
546
  };
499
547
 
500
- export { MARKUP, action, ai, grant, scrape };
548
+ export { MARKUP, action, ai, balance, grant, scrape };
package/dist/billing.d.ts CHANGED
@@ -318,10 +318,58 @@ const scrape = {
318
318
 
319
319
  };
320
320
 
321
+ // ─── Balance (derived from the ledger) ──────────────────────────────────────────
322
+
323
+ // The AI credit balance is DERIVED, never stored — so it cannot drift from the
324
+ // ledger. balance = the newest checkpoint's cumulative value + Σ amount of the
325
+ // rows after it (credits +, debits −); with no checkpoint it sums the whole
326
+ // ledger. Checkpoints (`balanceCheckpoint`) are immutable rollup rows written by
327
+ // drawbridge-sync's nightly job to bound the scan on long-lived accounts — this
328
+ // reader tolerates their absence and falls back to the full sum. The
329
+ // { user:1, type:1, _id:1, amount:1 } index on `transaction` keeps both the full
330
+ // and post-checkpoint sums index-only.
331
+ //
332
+ // `user` accepts an id string or a user object (uses `.id`). `db` is any
333
+ // @drawbridge/mongodb controller (uses `.aggregate`). Returns integer cents;
334
+ // MAY be negative when a generation over-spent past zero — callers clamp the
335
+ // DISPLAY to 0, while the credit gate treats `<= 0` as "no credits".
336
+ const balance = async ({ db, user, type }) => {
337
+
338
+ const userId = ( user && user.id ) || user;
339
+
340
+ if( ! db || ! userId || ! type ) return 0;
341
+
342
+ // Newest immutable checkpoint: its `value` is the cumulative balance through
343
+ // `throughId`, so we only sum rows inserted after it. Missing collection / no
344
+ // checkpoint yet → [] and we sum the whole ledger.
345
+ const [ checkpoint ] = await db.aggregate({
346
+ collection : 'balanceCheckpoint',
347
+ pipeline : [
348
+ { $match : { user : userId, type } },
349
+ { $sort : { _id : -1 } },
350
+ { $limit : 1 }
351
+ ]
352
+ }).catch( () => [] );
353
+
354
+ const match = { user : userId, type };
355
+ if( checkpoint?.throughId ) match._id = { $gt : checkpoint.throughId };
356
+
357
+ const [ summed ] = await db.aggregate({
358
+ collection : 'transaction',
359
+ pipeline : [
360
+ { $match : match },
361
+ { $group : { _id : null, total : { $sum : '$amount' } } }
362
+ ]
363
+ });
364
+
365
+ return ( checkpoint?.value || 0 ) + ( summed?.total || 0 );
366
+
367
+ };
368
+
321
369
  // ─── Grants (credit rail) ──────────────────────────────────────────────────────
322
370
 
323
- // Promotional credits to a user's AI balance. `source` captures who acted; the
324
- // stream listener on the transaction collection does the balance.ai $inc.
371
+ // Promotional credits to a user's AI balance append-only rows the derived
372
+ // `balance` sums. `source` captures who acted.
325
373
  const grant = {
326
374
 
327
375
  // System-issued credit (signup welcome, migration catch-up). Pass `session`
@@ -497,4 +545,4 @@ const action = {
497
545
 
498
546
  };
499
547
 
500
- export { MARKUP, action, ai, grant, scrape };
548
+ export { MARKUP, action, ai, balance, grant, scrape };
package/dist/billing.js CHANGED
@@ -13,9 +13,6 @@ var insertTransaction = async ({
13
13
  session,
14
14
  ...rest
15
15
  }) => {
16
- var _a;
17
- const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
18
- const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
19
16
  const trace = currentTraceId();
20
17
  await db.create({
21
18
  authenticated: user,
@@ -27,7 +24,6 @@ var insertTransaction = async ({
27
24
  category,
28
25
  source,
29
26
  amount,
30
- ...balance && { balance },
31
27
  ...trace && { trace },
32
28
  ...rest,
33
29
  ...stripeInvoiceId && { stripeInvoiceId },
@@ -312,6 +308,28 @@ var scrape = {
312
308
  }
313
309
  }
314
310
  };
311
+ var balance = async ({ db, user, type }) => {
312
+ const userId = user && user.id || user;
313
+ if (!db || !userId || !type) return 0;
314
+ const [checkpoint] = await db.aggregate({
315
+ collection: "balanceCheckpoint",
316
+ pipeline: [
317
+ { $match: { user: userId, type } },
318
+ { $sort: { _id: -1 } },
319
+ { $limit: 1 }
320
+ ]
321
+ }).catch(() => []);
322
+ const match = { user: userId, type };
323
+ if (checkpoint == null ? void 0 : checkpoint.throughId) match._id = { $gt: checkpoint.throughId };
324
+ const [summed] = await db.aggregate({
325
+ collection: "transaction",
326
+ pipeline: [
327
+ { $match: match },
328
+ { $group: { _id: null, total: { $sum: "$amount" } } }
329
+ ]
330
+ });
331
+ return ((checkpoint == null ? void 0 : checkpoint.value) || 0) + ((summed == null ? void 0 : summed.total) || 0);
332
+ };
315
333
  var grant = {
316
334
  // System-issued credit (signup welcome, migration catch-up). Pass `session`
317
335
  // to enroll the credit in the caller's transaction.
@@ -441,6 +459,7 @@ export {
441
459
  MARKUP,
442
460
  action,
443
461
  ai,
462
+ balance,
444
463
  grant,
445
464
  scrape
446
465
  };
@@ -37,9 +37,6 @@ var insertTransaction = async ({
37
37
  session,
38
38
  ...rest
39
39
  }) => {
40
- var _a;
41
- const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
42
- const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
43
40
  const trace = (0, import_drawbridge_telemetry.currentTraceId)();
44
41
  await db.create({
45
42
  authenticated: user,
@@ -51,7 +48,6 @@ var insertTransaction = async ({
51
48
  category,
52
49
  source,
53
50
  amount,
54
- ...balance && { balance },
55
51
  ...trace && { trace },
56
52
  ...rest,
57
53
  ...stripeInvoiceId && { stripeInvoiceId },
@@ -2,12 +2,13 @@ import { currentTraceId } from '@drawbridge/drawbridge-telemetry';
2
2
 
3
3
  // Ledger transaction insert helpers — no balance writes happen here.
4
4
  //
5
- // User-balance updates are the exclusive job of drawbridge-sync's
6
- // stream/transaction.js change-stream listener: every insert into the
7
- // `transaction` collection triggers an atomic $inc on user.balance.<type>
8
- // (guarded against going negative). This decouples the API surface from
9
- // the balance arithmetic and makes the `transaction` collection the single
10
- // source of truth for both audit history and balance derivation.
5
+ // The `transaction` collection is the SINGLE SOURCE OF TRUTH for balance:
6
+ // there is no stored balance field anywhere. The balance is DERIVED by summing
7
+ // a user's rows on read (see `balance` in billing.js) credits (+) and debits
8
+ // (−) so it can never drift from the ledger. credit()/debit() only append
9
+ // rows; drawbridge-sync's stream/transaction.js reacts to each insert to fire
10
+ // low/depleted notifications and emit `account.update` (it no longer maintains
11
+ // a balance).
11
12
  //
12
13
  // import { credit, debit } from '@drawbridge/drawbridge-utils/transactions';
13
14
  //
@@ -21,12 +22,9 @@ import { currentTraceId } from '@drawbridge/drawbridge-telemetry';
21
22
  // `db` is the wrapper returned by @drawbridge/mongodb — credit/debit
22
23
  // duck-type against its `.create` interface so this module has no hard dep.
23
24
  //
24
- // `user` is the user the transaction is being recorded against. The helper
25
- // reads `user.balance.<type>` to stamp `balance: { before, after }` on the
26
- // row as an audit snapshot of what the actor saw at action time — useful
27
- // for support / dispute resolution. When `balance.<type>` isn't on the user
28
- // (fresh signup, webhook-initiated row), the snapshot is omitted, which is
29
- // the honest representation of "no prior view to record".
25
+ // `user` is the user the transaction is being recorded against (stored as
26
+ // `user.id`). No per-row balance snapshot is stamped the running balance at
27
+ // any row is recoverable by summing the ledger up to it.
30
28
 
31
29
  const insertTransaction = async ({
32
30
  db,
@@ -42,10 +40,10 @@ const insertTransaction = async ({
42
40
  ...rest
43
41
  }) => {
44
42
 
45
- const beforeRaw = user?.balance?.[ type ];
46
- const balance = typeof beforeRaw === 'number'
47
- ? { before : beforeRaw, after : beforeRaw + amount }
48
- : undefined;
43
+ // No balance snapshot is stamped: the balance is DERIVED from this ledger
44
+ // (billing.balance sums it on read), so the running balance at any row is
45
+ // recoverable by summing up to that row a stored before/after would be
46
+ // redundant and, now that user.balance is gone, has no cheap source anyway.
49
47
 
50
48
  // Pull the active Sentry trace ID so every credit/debit auto-correlates
51
49
  // to the originating HTTP request or BullMQ job — useful for grouping
@@ -65,7 +63,6 @@ const insertTransaction = async ({
65
63
  category,
66
64
  source,
67
65
  amount,
68
- ...( balance && { balance }),
69
66
  ...( trace && { trace }),
70
67
  ...rest,
71
68
  ...( stripeInvoiceId && { stripeInvoiceId }),
@@ -2,12 +2,13 @@ import { currentTraceId } from '@drawbridge/drawbridge-telemetry';
2
2
 
3
3
  // Ledger transaction insert helpers — no balance writes happen here.
4
4
  //
5
- // User-balance updates are the exclusive job of drawbridge-sync's
6
- // stream/transaction.js change-stream listener: every insert into the
7
- // `transaction` collection triggers an atomic $inc on user.balance.<type>
8
- // (guarded against going negative). This decouples the API surface from
9
- // the balance arithmetic and makes the `transaction` collection the single
10
- // source of truth for both audit history and balance derivation.
5
+ // The `transaction` collection is the SINGLE SOURCE OF TRUTH for balance:
6
+ // there is no stored balance field anywhere. The balance is DERIVED by summing
7
+ // a user's rows on read (see `balance` in billing.js) credits (+) and debits
8
+ // (−) so it can never drift from the ledger. credit()/debit() only append
9
+ // rows; drawbridge-sync's stream/transaction.js reacts to each insert to fire
10
+ // low/depleted notifications and emit `account.update` (it no longer maintains
11
+ // a balance).
11
12
  //
12
13
  // import { credit, debit } from '@drawbridge/drawbridge-utils/transactions';
13
14
  //
@@ -21,12 +22,9 @@ import { currentTraceId } from '@drawbridge/drawbridge-telemetry';
21
22
  // `db` is the wrapper returned by @drawbridge/mongodb — credit/debit
22
23
  // duck-type against its `.create` interface so this module has no hard dep.
23
24
  //
24
- // `user` is the user the transaction is being recorded against. The helper
25
- // reads `user.balance.<type>` to stamp `balance: { before, after }` on the
26
- // row as an audit snapshot of what the actor saw at action time — useful
27
- // for support / dispute resolution. When `balance.<type>` isn't on the user
28
- // (fresh signup, webhook-initiated row), the snapshot is omitted, which is
29
- // the honest representation of "no prior view to record".
25
+ // `user` is the user the transaction is being recorded against (stored as
26
+ // `user.id`). No per-row balance snapshot is stamped the running balance at
27
+ // any row is recoverable by summing the ledger up to it.
30
28
 
31
29
  const insertTransaction = async ({
32
30
  db,
@@ -42,10 +40,10 @@ const insertTransaction = async ({
42
40
  ...rest
43
41
  }) => {
44
42
 
45
- const beforeRaw = user?.balance?.[ type ];
46
- const balance = typeof beforeRaw === 'number'
47
- ? { before : beforeRaw, after : beforeRaw + amount }
48
- : undefined;
43
+ // No balance snapshot is stamped: the balance is DERIVED from this ledger
44
+ // (billing.balance sums it on read), so the running balance at any row is
45
+ // recoverable by summing up to that row a stored before/after would be
46
+ // redundant and, now that user.balance is gone, has no cheap source anyway.
49
47
 
50
48
  // Pull the active Sentry trace ID so every credit/debit auto-correlates
51
49
  // to the originating HTTP request or BullMQ job — useful for grouping
@@ -65,7 +63,6 @@ const insertTransaction = async ({
65
63
  category,
66
64
  source,
67
65
  amount,
68
- ...( balance && { balance }),
69
66
  ...( trace && { trace }),
70
67
  ...rest,
71
68
  ...( stripeInvoiceId && { stripeInvoiceId }),
@@ -13,9 +13,6 @@ var insertTransaction = async ({
13
13
  session,
14
14
  ...rest
15
15
  }) => {
16
- var _a;
17
- const beforeRaw = (_a = user == null ? void 0 : user.balance) == null ? void 0 : _a[type];
18
- const balance = typeof beforeRaw === "number" ? { before: beforeRaw, after: beforeRaw + amount } : void 0;
19
16
  const trace = currentTraceId();
20
17
  await db.create({
21
18
  authenticated: user,
@@ -27,7 +24,6 @@ var insertTransaction = async ({
27
24
  category,
28
25
  source,
29
26
  amount,
30
- ...balance && { balance },
31
27
  ...trace && { trace },
32
28
  ...rest,
33
29
  ...stripeInvoiceId && { stripeInvoiceId },
package/package.json CHANGED
@@ -164,5 +164,5 @@
164
164
  "test": "node --test test/"
165
165
  },
166
166
  "types": "dist/index.d.ts",
167
- "version": "0.0.80"
167
+ "version": "0.0.81"
168
168
  }