@drawbridge/drawbridge-utils 0.0.29 → 0.0.32
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 +120 -135
- package/dist/ai.d.cts +117 -96
- package/dist/ai.d.ts +117 -96
- package/dist/ai.js +85 -71
- package/dist/chunk-RW2FJVIV.js +110 -0
- package/dist/transactions.cjs +51 -104
- package/dist/transactions.d.cts +93 -139
- package/dist/transactions.d.ts +93 -139
- package/dist/transactions.js +1 -3
- package/package.json +1 -1
- package/dist/chunk-H735KDYS.js +0 -162
package/dist/transactions.d.ts
CHANGED
|
@@ -1,56 +1,62 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Ledger transaction insert helpers — no balance writes happen here.
|
|
2
|
+
//
|
|
3
|
+
// User-balance updates are the exclusive job of drawbridge-sync's
|
|
4
|
+
// stream/transaction.js change-stream listener: every insert into the
|
|
5
|
+
// `transaction` collection triggers an atomic $inc on user.balance.<type>
|
|
6
|
+
// (guarded against going negative). This decouples the API surface from
|
|
7
|
+
// the balance arithmetic and makes the `transaction` collection the single
|
|
8
|
+
// source of truth for both audit history and balance derivation.
|
|
5
9
|
//
|
|
6
10
|
// import { credit, debit } from '@drawbridge/drawbridge-utils/transactions';
|
|
7
11
|
//
|
|
8
|
-
// await credit({ db,
|
|
9
|
-
// category: 'promotional',
|
|
12
|
+
// await credit({ db, user, amount: 500,
|
|
13
|
+
// type: 'ai', category: 'promotional', source: 'system' });
|
|
10
14
|
//
|
|
11
|
-
// await debit({ db,
|
|
12
|
-
//
|
|
13
|
-
//
|
|
15
|
+
// await debit({ db, user, amount: 6,
|
|
16
|
+
// type: 'ai', category: 'usage', source: 'user',
|
|
17
|
+
// ai: { name, model, tokens, rates, totals } });
|
|
14
18
|
//
|
|
15
|
-
// `db` is the wrapper returned by @drawbridge/mongodb — credit/debit
|
|
16
|
-
// against its `.create
|
|
19
|
+
// `db` is the wrapper returned by @drawbridge/mongodb — credit/debit
|
|
20
|
+
// duck-type against its `.create` interface so this module has no hard dep.
|
|
17
21
|
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// the
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// contract.
|
|
27
|
-
const recordTransaction = async ({
|
|
22
|
+
// `user` is the user the transaction is being recorded against. The helper
|
|
23
|
+
// reads `user.balance.<type>` to stamp `balance: { before, after }` on the
|
|
24
|
+
// row as an audit snapshot of what the actor saw at action time — useful
|
|
25
|
+
// for support / dispute resolution. When `balance.<type>` isn't on the user
|
|
26
|
+
// (fresh signup, webhook-initiated row), the snapshot is omitted, which is
|
|
27
|
+
// the honest representation of "no prior view to record".
|
|
28
|
+
|
|
29
|
+
const insertTransaction = async ({
|
|
28
30
|
db,
|
|
29
|
-
|
|
30
|
-
|
|
31
|
+
user,
|
|
32
|
+
type,
|
|
31
33
|
category,
|
|
32
|
-
|
|
34
|
+
source,
|
|
33
35
|
amount,
|
|
34
|
-
|
|
35
|
-
provider,
|
|
36
|
+
_id,
|
|
36
37
|
stripeInvoiceId,
|
|
37
38
|
stripeEventId,
|
|
38
|
-
session
|
|
39
|
+
session,
|
|
40
|
+
...rest
|
|
39
41
|
}) => {
|
|
40
42
|
|
|
43
|
+
const beforeRaw = user?.balance?.[ type ];
|
|
44
|
+
const balance = typeof beforeRaw === 'number'
|
|
45
|
+
? { before : beforeRaw, after : beforeRaw + amount }
|
|
46
|
+
: undefined;
|
|
47
|
+
|
|
41
48
|
await db.create({
|
|
42
|
-
authenticated,
|
|
49
|
+
authenticated : user,
|
|
43
50
|
collection : 'transaction',
|
|
44
51
|
data : {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
},
|
|
52
|
+
...( _id && { _id }),
|
|
53
|
+
userId : user?.id,
|
|
54
|
+
type,
|
|
49
55
|
category,
|
|
50
|
-
|
|
56
|
+
source,
|
|
51
57
|
amount,
|
|
52
|
-
balance,
|
|
53
|
-
...
|
|
58
|
+
...( balance && { balance }),
|
|
59
|
+
...rest,
|
|
54
60
|
...( stripeInvoiceId && { stripeInvoiceId }),
|
|
55
61
|
...( stripeEventId && { stripeEventId })
|
|
56
62
|
},
|
|
@@ -59,20 +65,22 @@ const recordTransaction = async ({
|
|
|
59
65
|
|
|
60
66
|
};
|
|
61
67
|
|
|
62
|
-
// Additive:
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
68
|
+
// Additive: record a credit. The stream handler does the $inc separately.
|
|
69
|
+
// Optional `_id` lets callers pre-generate the transaction's ObjectId — the
|
|
70
|
+
// top-up route stamps it onto the Stripe invoice metadata so Stripe-side and
|
|
71
|
+
// Mongo-side IDs match for cross-system reconciliation.
|
|
66
72
|
const credit = async ({
|
|
67
73
|
db,
|
|
68
|
-
|
|
69
|
-
userId,
|
|
74
|
+
user,
|
|
70
75
|
amount,
|
|
76
|
+
type,
|
|
71
77
|
category,
|
|
72
|
-
|
|
78
|
+
source,
|
|
79
|
+
_id,
|
|
73
80
|
stripeInvoiceId,
|
|
74
81
|
stripeEventId,
|
|
75
|
-
session
|
|
82
|
+
session,
|
|
83
|
+
...rest
|
|
76
84
|
}) => {
|
|
77
85
|
|
|
78
86
|
if( !Number.isInteger( amount ) || amount <= 0 ){
|
|
@@ -81,83 +89,56 @@ const credit = async ({
|
|
|
81
89
|
|
|
82
90
|
}
|
|
83
91
|
|
|
84
|
-
|
|
85
|
-
authenticated,
|
|
86
|
-
collection : 'user',
|
|
87
|
-
query : {
|
|
88
|
-
id : userId
|
|
89
|
-
},
|
|
90
|
-
data : {
|
|
91
|
-
$inc : {
|
|
92
|
-
'balance.ai' : amount
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
...( session && { options : { session } })
|
|
96
|
-
});
|
|
92
|
+
if( !type ){
|
|
97
93
|
|
|
98
|
-
|
|
94
|
+
throw new Error( 'credit() requires a type (e.g., "ai")' );
|
|
99
95
|
|
|
100
|
-
|
|
96
|
+
}
|
|
101
97
|
|
|
102
|
-
|
|
98
|
+
if( !source ){
|
|
103
99
|
|
|
104
|
-
|
|
100
|
+
throw new Error( 'credit() requires a source ("system" | "admin" | "user")' );
|
|
105
101
|
|
|
106
|
-
|
|
102
|
+
}
|
|
107
103
|
|
|
108
|
-
await
|
|
104
|
+
await insertTransaction({
|
|
109
105
|
db,
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
user,
|
|
107
|
+
type,
|
|
112
108
|
category,
|
|
113
|
-
|
|
109
|
+
source,
|
|
114
110
|
amount,
|
|
115
|
-
|
|
116
|
-
before,
|
|
117
|
-
after
|
|
118
|
-
},
|
|
111
|
+
_id,
|
|
119
112
|
stripeInvoiceId,
|
|
120
113
|
stripeEventId,
|
|
121
|
-
session
|
|
114
|
+
session,
|
|
115
|
+
...rest
|
|
122
116
|
});
|
|
123
117
|
|
|
124
|
-
return {
|
|
125
|
-
balance : {
|
|
126
|
-
before,
|
|
127
|
-
after
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
118
|
};
|
|
132
119
|
|
|
133
|
-
// Subtractive:
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this.status = 402;
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
120
|
+
// Subtractive: record a debit (negative amount). The stream handler's $inc
|
|
121
|
+
// is guarded against negative balance — if the resulting balance would be
|
|
122
|
+
// less than zero, the $inc is skipped and the row stays as audit-only.
|
|
123
|
+
// Callers (e.g., ai.js) pre-compute `amount` from their domain-specific
|
|
124
|
+
// pricing.
|
|
125
|
+
//
|
|
126
|
+
// Note: there is no synchronous gate here. The atomic gate-and-decrement
|
|
127
|
+
// pattern moves to the AI middleware (`balance.<type> >= MAX_REQUEST_CENTS`)
|
|
128
|
+
// plus the stream-side $inc guard. Concurrent debits with low balance can
|
|
129
|
+
// race past the middleware check but the stream guard prevents the actual
|
|
130
|
+
// balance from going negative.
|
|
150
131
|
const debit = async ({
|
|
151
132
|
db,
|
|
152
|
-
|
|
153
|
-
userId,
|
|
133
|
+
user,
|
|
154
134
|
amount,
|
|
135
|
+
type,
|
|
155
136
|
category,
|
|
156
|
-
|
|
157
|
-
provider,
|
|
137
|
+
source,
|
|
158
138
|
stripeInvoiceId,
|
|
159
139
|
stripeEventId,
|
|
160
|
-
session
|
|
140
|
+
session,
|
|
141
|
+
...rest
|
|
161
142
|
}) => {
|
|
162
143
|
|
|
163
144
|
if( !Number.isInteger( amount ) || amount <= 0 ){
|
|
@@ -166,58 +147,31 @@ const debit = async ({
|
|
|
166
147
|
|
|
167
148
|
}
|
|
168
149
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
query : {
|
|
173
|
-
id : userId,
|
|
174
|
-
'balance.ai' : {
|
|
175
|
-
$gte : amount
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
data : {
|
|
179
|
-
$inc : {
|
|
180
|
-
'balance.ai' : -amount
|
|
181
|
-
}
|
|
182
|
-
},
|
|
183
|
-
...( session && { options : { session } })
|
|
184
|
-
});
|
|
150
|
+
if( !type ){
|
|
151
|
+
|
|
152
|
+
throw new Error( 'debit() requires a type (e.g., "ai")' );
|
|
185
153
|
|
|
186
|
-
|
|
154
|
+
}
|
|
187
155
|
|
|
188
|
-
if(
|
|
156
|
+
if( !source ){
|
|
189
157
|
|
|
190
|
-
throw new
|
|
158
|
+
throw new Error( 'debit() requires a source ("system" | "admin" | "user")' );
|
|
191
159
|
|
|
192
160
|
}
|
|
193
161
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
await recordTransaction({
|
|
162
|
+
await insertTransaction({
|
|
197
163
|
db,
|
|
198
|
-
|
|
199
|
-
|
|
164
|
+
user,
|
|
165
|
+
type,
|
|
200
166
|
category,
|
|
201
|
-
|
|
167
|
+
source,
|
|
202
168
|
amount : -amount,
|
|
203
|
-
balance : {
|
|
204
|
-
before,
|
|
205
|
-
after
|
|
206
|
-
},
|
|
207
|
-
provider,
|
|
208
169
|
stripeInvoiceId,
|
|
209
170
|
stripeEventId,
|
|
210
|
-
session
|
|
171
|
+
session,
|
|
172
|
+
...rest
|
|
211
173
|
});
|
|
212
174
|
|
|
213
|
-
return {
|
|
214
|
-
amount : -amount,
|
|
215
|
-
balance : {
|
|
216
|
-
before,
|
|
217
|
-
after
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
|
|
221
175
|
};
|
|
222
176
|
|
|
223
|
-
export {
|
|
177
|
+
export { credit, debit };
|
package/dist/transactions.js
CHANGED
package/package.json
CHANGED
package/dist/chunk-H735KDYS.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
// transactions.js
|
|
2
|
-
var recordTransaction = async ({
|
|
3
|
-
db,
|
|
4
|
-
authenticated,
|
|
5
|
-
userId,
|
|
6
|
-
category,
|
|
7
|
-
tags,
|
|
8
|
-
amount,
|
|
9
|
-
balance,
|
|
10
|
-
provider,
|
|
11
|
-
stripeInvoiceId,
|
|
12
|
-
stripeEventId,
|
|
13
|
-
session
|
|
14
|
-
}) => {
|
|
15
|
-
await db.create({
|
|
16
|
-
authenticated,
|
|
17
|
-
collection: "transaction",
|
|
18
|
-
data: {
|
|
19
|
-
userId,
|
|
20
|
-
billable: {
|
|
21
|
-
item: "ai"
|
|
22
|
-
},
|
|
23
|
-
category,
|
|
24
|
-
tags,
|
|
25
|
-
amount,
|
|
26
|
-
balance,
|
|
27
|
-
...provider && { provider },
|
|
28
|
-
...stripeInvoiceId && { stripeInvoiceId },
|
|
29
|
-
...stripeEventId && { stripeEventId }
|
|
30
|
-
},
|
|
31
|
-
...session && { options: { session } }
|
|
32
|
-
});
|
|
33
|
-
};
|
|
34
|
-
var credit = async ({
|
|
35
|
-
db,
|
|
36
|
-
authenticated,
|
|
37
|
-
userId,
|
|
38
|
-
amount,
|
|
39
|
-
category,
|
|
40
|
-
tags = [],
|
|
41
|
-
stripeInvoiceId,
|
|
42
|
-
stripeEventId,
|
|
43
|
-
session
|
|
44
|
-
}) => {
|
|
45
|
-
var _a, _b, _c;
|
|
46
|
-
if (!Number.isInteger(amount) || amount <= 0) {
|
|
47
|
-
throw new Error(`credit() requires a positive integer amount, got ${amount}`);
|
|
48
|
-
}
|
|
49
|
-
const result = await db.update({
|
|
50
|
-
authenticated,
|
|
51
|
-
collection: "user",
|
|
52
|
-
query: {
|
|
53
|
-
id: userId
|
|
54
|
-
},
|
|
55
|
-
data: {
|
|
56
|
-
$inc: {
|
|
57
|
-
"balance.ai": amount
|
|
58
|
-
}
|
|
59
|
-
},
|
|
60
|
-
...session && { options: { session } }
|
|
61
|
-
});
|
|
62
|
-
const after = ((_b = (_a = result == null ? void 0 : result.value) == null ? void 0 : _a.balance) == null ? void 0 : _b.ai) ?? ((_c = result == null ? void 0 : result.balance) == null ? void 0 : _c.ai);
|
|
63
|
-
if (typeof after !== "number") {
|
|
64
|
-
throw new Error(`credit() could not read updated balance for user ${userId}`);
|
|
65
|
-
}
|
|
66
|
-
const before = after - amount;
|
|
67
|
-
await recordTransaction({
|
|
68
|
-
db,
|
|
69
|
-
authenticated,
|
|
70
|
-
userId,
|
|
71
|
-
category,
|
|
72
|
-
tags,
|
|
73
|
-
amount,
|
|
74
|
-
balance: {
|
|
75
|
-
before,
|
|
76
|
-
after
|
|
77
|
-
},
|
|
78
|
-
stripeInvoiceId,
|
|
79
|
-
stripeEventId,
|
|
80
|
-
session
|
|
81
|
-
});
|
|
82
|
-
return {
|
|
83
|
-
balance: {
|
|
84
|
-
before,
|
|
85
|
-
after
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
};
|
|
89
|
-
var InsufficientCreditsError = class extends Error {
|
|
90
|
-
constructor(message = "Insufficient AI credits") {
|
|
91
|
-
super(message);
|
|
92
|
-
this.name = "InsufficientCreditsError";
|
|
93
|
-
this.status = 402;
|
|
94
|
-
}
|
|
95
|
-
};
|
|
96
|
-
var debit = async ({
|
|
97
|
-
db,
|
|
98
|
-
authenticated,
|
|
99
|
-
userId,
|
|
100
|
-
amount,
|
|
101
|
-
category,
|
|
102
|
-
tags = [],
|
|
103
|
-
provider,
|
|
104
|
-
stripeInvoiceId,
|
|
105
|
-
stripeEventId,
|
|
106
|
-
session
|
|
107
|
-
}) => {
|
|
108
|
-
var _a, _b, _c;
|
|
109
|
-
if (!Number.isInteger(amount) || amount <= 0) {
|
|
110
|
-
throw new Error(`debit() requires a positive integer amount, got ${amount}`);
|
|
111
|
-
}
|
|
112
|
-
const result = await db.update({
|
|
113
|
-
authenticated,
|
|
114
|
-
collection: "user",
|
|
115
|
-
query: {
|
|
116
|
-
id: userId,
|
|
117
|
-
"balance.ai": {
|
|
118
|
-
$gte: amount
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
data: {
|
|
122
|
-
$inc: {
|
|
123
|
-
"balance.ai": -amount
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
...session && { options: { session } }
|
|
127
|
-
});
|
|
128
|
-
const after = ((_b = (_a = result == null ? void 0 : result.value) == null ? void 0 : _a.balance) == null ? void 0 : _b.ai) ?? ((_c = result == null ? void 0 : result.balance) == null ? void 0 : _c.ai);
|
|
129
|
-
if (typeof after !== "number") {
|
|
130
|
-
throw new InsufficientCreditsError();
|
|
131
|
-
}
|
|
132
|
-
const before = after + amount;
|
|
133
|
-
await recordTransaction({
|
|
134
|
-
db,
|
|
135
|
-
authenticated,
|
|
136
|
-
userId,
|
|
137
|
-
category,
|
|
138
|
-
tags,
|
|
139
|
-
amount: -amount,
|
|
140
|
-
balance: {
|
|
141
|
-
before,
|
|
142
|
-
after
|
|
143
|
-
},
|
|
144
|
-
provider,
|
|
145
|
-
stripeInvoiceId,
|
|
146
|
-
stripeEventId,
|
|
147
|
-
session
|
|
148
|
-
});
|
|
149
|
-
return {
|
|
150
|
-
amount: -amount,
|
|
151
|
-
balance: {
|
|
152
|
-
before,
|
|
153
|
-
after
|
|
154
|
-
}
|
|
155
|
-
};
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
export {
|
|
159
|
-
credit,
|
|
160
|
-
InsufficientCreditsError,
|
|
161
|
-
debit
|
|
162
|
-
};
|