@drawbridge/drawbridge-utils 0.0.29 → 0.0.31
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 +76 -74
- package/dist/ai.d.cts +72 -29
- package/dist/ai.d.ts +72 -29
- package/dist/ai.js +42 -14
- package/dist/chunk-6S43OFA7.js +115 -0
- package/dist/transactions.cjs +50 -98
- package/dist/transactions.d.cts +92 -133
- package/dist/transactions.d.ts +92 -133
- 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,63 @@
|
|
|
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
|
-
|
|
31
|
+
user,
|
|
30
32
|
userId,
|
|
33
|
+
type,
|
|
31
34
|
category,
|
|
32
|
-
|
|
35
|
+
source,
|
|
33
36
|
amount,
|
|
34
|
-
|
|
35
|
-
provider,
|
|
37
|
+
_id,
|
|
36
38
|
stripeInvoiceId,
|
|
37
39
|
stripeEventId,
|
|
38
|
-
session
|
|
40
|
+
session,
|
|
41
|
+
...rest
|
|
39
42
|
}) => {
|
|
40
43
|
|
|
44
|
+
const beforeRaw = user?.balance?.[ type ];
|
|
45
|
+
const balance = typeof beforeRaw === 'number'
|
|
46
|
+
? { before : beforeRaw, after : beforeRaw + amount }
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
41
49
|
await db.create({
|
|
42
|
-
authenticated,
|
|
50
|
+
authenticated : user,
|
|
43
51
|
collection : 'transaction',
|
|
44
52
|
data : {
|
|
53
|
+
...( _id && { _id }),
|
|
45
54
|
userId,
|
|
46
|
-
|
|
47
|
-
item : 'ai'
|
|
48
|
-
},
|
|
55
|
+
type,
|
|
49
56
|
category,
|
|
50
|
-
|
|
57
|
+
source,
|
|
51
58
|
amount,
|
|
52
|
-
balance,
|
|
53
|
-
...
|
|
59
|
+
...( balance && { balance }),
|
|
60
|
+
...rest,
|
|
54
61
|
...( stripeInvoiceId && { stripeInvoiceId }),
|
|
55
62
|
...( stripeEventId && { stripeEventId })
|
|
56
63
|
},
|
|
@@ -59,20 +66,23 @@ const recordTransaction = async ({
|
|
|
59
66
|
|
|
60
67
|
};
|
|
61
68
|
|
|
62
|
-
// Additive:
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
69
|
+
// Additive: record a credit. The stream handler does the $inc separately.
|
|
70
|
+
// Optional `_id` lets callers pre-generate the transaction's ObjectId — the
|
|
71
|
+
// top-up route stamps it onto the Stripe invoice metadata so Stripe-side and
|
|
72
|
+
// Mongo-side IDs match for cross-system reconciliation.
|
|
66
73
|
const credit = async ({
|
|
67
74
|
db,
|
|
68
|
-
|
|
75
|
+
user,
|
|
69
76
|
userId,
|
|
70
77
|
amount,
|
|
78
|
+
type,
|
|
71
79
|
category,
|
|
72
|
-
|
|
80
|
+
source,
|
|
81
|
+
_id,
|
|
73
82
|
stripeInvoiceId,
|
|
74
83
|
stripeEventId,
|
|
75
|
-
session
|
|
84
|
+
session,
|
|
85
|
+
...rest
|
|
76
86
|
}) => {
|
|
77
87
|
|
|
78
88
|
if( !Number.isInteger( amount ) || amount <= 0 ){
|
|
@@ -81,83 +91,58 @@ const credit = async ({
|
|
|
81
91
|
|
|
82
92
|
}
|
|
83
93
|
|
|
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
|
-
});
|
|
94
|
+
if( !type ){
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
throw new Error( 'credit() requires a type (e.g., "ai")' );
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
}
|
|
101
99
|
|
|
102
|
-
|
|
100
|
+
if( !source ){
|
|
103
101
|
|
|
104
|
-
|
|
102
|
+
throw new Error( 'credit() requires a source ("system" | "admin" | "user")' );
|
|
105
103
|
|
|
106
|
-
|
|
104
|
+
}
|
|
107
105
|
|
|
108
|
-
await
|
|
106
|
+
await insertTransaction({
|
|
109
107
|
db,
|
|
110
|
-
|
|
108
|
+
user,
|
|
111
109
|
userId,
|
|
110
|
+
type,
|
|
112
111
|
category,
|
|
113
|
-
|
|
112
|
+
source,
|
|
114
113
|
amount,
|
|
115
|
-
|
|
116
|
-
before,
|
|
117
|
-
after
|
|
118
|
-
},
|
|
114
|
+
_id,
|
|
119
115
|
stripeInvoiceId,
|
|
120
116
|
stripeEventId,
|
|
121
|
-
session
|
|
117
|
+
session,
|
|
118
|
+
...rest
|
|
122
119
|
});
|
|
123
120
|
|
|
124
|
-
return {
|
|
125
|
-
balance : {
|
|
126
|
-
before,
|
|
127
|
-
after
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
121
|
};
|
|
132
122
|
|
|
133
|
-
// Subtractive:
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this.status = 402;
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
123
|
+
// Subtractive: record a debit (negative amount). The stream handler's $inc
|
|
124
|
+
// is guarded against negative balance — if the resulting balance would be
|
|
125
|
+
// less than zero, the $inc is skipped and the row stays as audit-only.
|
|
126
|
+
// Callers (e.g., ai.js) pre-compute `amount` from their domain-specific
|
|
127
|
+
// pricing.
|
|
128
|
+
//
|
|
129
|
+
// Note: there is no synchronous gate here. The atomic gate-and-decrement
|
|
130
|
+
// pattern moves to the AI middleware (`balance.<type> >= MAX_REQUEST_CENTS`)
|
|
131
|
+
// plus the stream-side $inc guard. Concurrent debits with low balance can
|
|
132
|
+
// race past the middleware check but the stream guard prevents the actual
|
|
133
|
+
// balance from going negative.
|
|
150
134
|
const debit = async ({
|
|
151
135
|
db,
|
|
152
|
-
|
|
136
|
+
user,
|
|
153
137
|
userId,
|
|
154
138
|
amount,
|
|
139
|
+
type,
|
|
155
140
|
category,
|
|
156
|
-
|
|
157
|
-
provider,
|
|
141
|
+
source,
|
|
158
142
|
stripeInvoiceId,
|
|
159
143
|
stripeEventId,
|
|
160
|
-
session
|
|
144
|
+
session,
|
|
145
|
+
...rest
|
|
161
146
|
}) => {
|
|
162
147
|
|
|
163
148
|
if( !Number.isInteger( amount ) || amount <= 0 ){
|
|
@@ -166,58 +151,32 @@ const debit = async ({
|
|
|
166
151
|
|
|
167
152
|
}
|
|
168
153
|
|
|
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
|
-
});
|
|
154
|
+
if( !type ){
|
|
155
|
+
|
|
156
|
+
throw new Error( 'debit() requires a type (e.g., "ai")' );
|
|
185
157
|
|
|
186
|
-
|
|
158
|
+
}
|
|
187
159
|
|
|
188
|
-
if(
|
|
160
|
+
if( !source ){
|
|
189
161
|
|
|
190
|
-
throw new
|
|
162
|
+
throw new Error( 'debit() requires a source ("system" | "admin" | "user")' );
|
|
191
163
|
|
|
192
164
|
}
|
|
193
165
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
await recordTransaction({
|
|
166
|
+
await insertTransaction({
|
|
197
167
|
db,
|
|
198
|
-
|
|
168
|
+
user,
|
|
199
169
|
userId,
|
|
170
|
+
type,
|
|
200
171
|
category,
|
|
201
|
-
|
|
172
|
+
source,
|
|
202
173
|
amount : -amount,
|
|
203
|
-
balance : {
|
|
204
|
-
before,
|
|
205
|
-
after
|
|
206
|
-
},
|
|
207
|
-
provider,
|
|
208
174
|
stripeInvoiceId,
|
|
209
175
|
stripeEventId,
|
|
210
|
-
session
|
|
176
|
+
session,
|
|
177
|
+
...rest
|
|
211
178
|
});
|
|
212
179
|
|
|
213
|
-
return {
|
|
214
|
-
amount : -amount,
|
|
215
|
-
balance : {
|
|
216
|
-
before,
|
|
217
|
-
after
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
|
|
221
180
|
};
|
|
222
181
|
|
|
223
|
-
export {
|
|
182
|
+
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
|
-
};
|