@drawbridge/drawbridge-utils 0.0.28 → 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 +82 -75
- 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 +58 -98
- package/dist/transactions.d.cts +101 -127
- package/dist/transactions.d.ts +101 -127
- package/dist/transactions.js +1 -3
- package/package.json +1 -1
- package/dist/chunk-RC5E56UB.js +0 -154
package/dist/transactions.d.ts
CHANGED
|
@@ -1,68 +1,88 @@
|
|
|
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
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
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.
|
|
21
|
+
//
|
|
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 ({
|
|
21
30
|
db,
|
|
22
|
-
|
|
31
|
+
user,
|
|
23
32
|
userId,
|
|
33
|
+
type,
|
|
24
34
|
category,
|
|
25
|
-
|
|
35
|
+
source,
|
|
26
36
|
amount,
|
|
27
|
-
|
|
28
|
-
provider,
|
|
37
|
+
_id,
|
|
29
38
|
stripeInvoiceId,
|
|
30
|
-
stripeEventId
|
|
39
|
+
stripeEventId,
|
|
40
|
+
session,
|
|
41
|
+
...rest
|
|
31
42
|
}) => {
|
|
32
43
|
|
|
44
|
+
const beforeRaw = user?.balance?.[ type ];
|
|
45
|
+
const balance = typeof beforeRaw === 'number'
|
|
46
|
+
? { before : beforeRaw, after : beforeRaw + amount }
|
|
47
|
+
: undefined;
|
|
48
|
+
|
|
33
49
|
await db.create({
|
|
34
|
-
authenticated,
|
|
35
|
-
collection : '
|
|
50
|
+
authenticated : user,
|
|
51
|
+
collection : 'transaction',
|
|
36
52
|
data : {
|
|
53
|
+
...( _id && { _id }),
|
|
37
54
|
userId,
|
|
38
|
-
|
|
39
|
-
item : 'ai'
|
|
40
|
-
},
|
|
55
|
+
type,
|
|
41
56
|
category,
|
|
42
|
-
|
|
57
|
+
source,
|
|
43
58
|
amount,
|
|
44
|
-
balance,
|
|
45
|
-
...
|
|
59
|
+
...( balance && { balance }),
|
|
60
|
+
...rest,
|
|
46
61
|
...( stripeInvoiceId && { stripeInvoiceId }),
|
|
47
62
|
...( stripeEventId && { stripeEventId })
|
|
48
|
-
}
|
|
63
|
+
},
|
|
64
|
+
...( session && { options : { session } })
|
|
49
65
|
});
|
|
50
66
|
|
|
51
67
|
};
|
|
52
68
|
|
|
53
|
-
// Additive:
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
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.
|
|
57
73
|
const credit = async ({
|
|
58
74
|
db,
|
|
59
|
-
|
|
75
|
+
user,
|
|
60
76
|
userId,
|
|
61
77
|
amount,
|
|
78
|
+
type,
|
|
62
79
|
category,
|
|
63
|
-
|
|
80
|
+
source,
|
|
81
|
+
_id,
|
|
64
82
|
stripeInvoiceId,
|
|
65
|
-
stripeEventId
|
|
83
|
+
stripeEventId,
|
|
84
|
+
session,
|
|
85
|
+
...rest
|
|
66
86
|
}) => {
|
|
67
87
|
|
|
68
88
|
if( !Number.isInteger( amount ) || amount <= 0 ){
|
|
@@ -71,80 +91,58 @@ const credit = async ({
|
|
|
71
91
|
|
|
72
92
|
}
|
|
73
93
|
|
|
74
|
-
|
|
75
|
-
authenticated,
|
|
76
|
-
collection : 'users',
|
|
77
|
-
query : {
|
|
78
|
-
id : userId
|
|
79
|
-
},
|
|
80
|
-
data : {
|
|
81
|
-
$inc : {
|
|
82
|
-
'balance.ai' : amount
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
});
|
|
94
|
+
if( !type ){
|
|
86
95
|
|
|
87
|
-
|
|
96
|
+
throw new Error( 'credit() requires a type (e.g., "ai")' );
|
|
88
97
|
|
|
89
|
-
|
|
98
|
+
}
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
if( !source ){
|
|
92
101
|
|
|
93
|
-
|
|
102
|
+
throw new Error( 'credit() requires a source ("system" | "admin" | "user")' );
|
|
94
103
|
|
|
95
|
-
|
|
104
|
+
}
|
|
96
105
|
|
|
97
|
-
await
|
|
106
|
+
await insertTransaction({
|
|
98
107
|
db,
|
|
99
|
-
|
|
108
|
+
user,
|
|
100
109
|
userId,
|
|
110
|
+
type,
|
|
101
111
|
category,
|
|
102
|
-
|
|
112
|
+
source,
|
|
103
113
|
amount,
|
|
104
|
-
|
|
105
|
-
before,
|
|
106
|
-
after
|
|
107
|
-
},
|
|
114
|
+
_id,
|
|
108
115
|
stripeInvoiceId,
|
|
109
|
-
stripeEventId
|
|
116
|
+
stripeEventId,
|
|
117
|
+
session,
|
|
118
|
+
...rest
|
|
110
119
|
});
|
|
111
120
|
|
|
112
|
-
return {
|
|
113
|
-
balance : {
|
|
114
|
-
before,
|
|
115
|
-
after
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
121
|
};
|
|
120
122
|
|
|
121
|
-
// Subtractive:
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this.status = 402;
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
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.
|
|
138
134
|
const debit = async ({
|
|
139
135
|
db,
|
|
140
|
-
|
|
136
|
+
user,
|
|
141
137
|
userId,
|
|
142
138
|
amount,
|
|
139
|
+
type,
|
|
143
140
|
category,
|
|
144
|
-
|
|
145
|
-
provider,
|
|
141
|
+
source,
|
|
146
142
|
stripeInvoiceId,
|
|
147
|
-
stripeEventId
|
|
143
|
+
stripeEventId,
|
|
144
|
+
session,
|
|
145
|
+
...rest
|
|
148
146
|
}) => {
|
|
149
147
|
|
|
150
148
|
if( !Number.isInteger( amount ) || amount <= 0 ){
|
|
@@ -153,56 +151,32 @@ const debit = async ({
|
|
|
153
151
|
|
|
154
152
|
}
|
|
155
153
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
query : {
|
|
160
|
-
id : userId,
|
|
161
|
-
'balance.ai' : {
|
|
162
|
-
$gte : amount
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
data : {
|
|
166
|
-
$inc : {
|
|
167
|
-
'balance.ai' : -amount
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
});
|
|
154
|
+
if( !type ){
|
|
155
|
+
|
|
156
|
+
throw new Error( 'debit() requires a type (e.g., "ai")' );
|
|
171
157
|
|
|
172
|
-
|
|
158
|
+
}
|
|
173
159
|
|
|
174
|
-
if(
|
|
160
|
+
if( !source ){
|
|
175
161
|
|
|
176
|
-
throw new
|
|
162
|
+
throw new Error( 'debit() requires a source ("system" | "admin" | "user")' );
|
|
177
163
|
|
|
178
164
|
}
|
|
179
165
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
await recordTransaction({
|
|
166
|
+
await insertTransaction({
|
|
183
167
|
db,
|
|
184
|
-
|
|
168
|
+
user,
|
|
185
169
|
userId,
|
|
170
|
+
type,
|
|
186
171
|
category,
|
|
187
|
-
|
|
172
|
+
source,
|
|
188
173
|
amount : -amount,
|
|
189
|
-
balance : {
|
|
190
|
-
before,
|
|
191
|
-
after
|
|
192
|
-
},
|
|
193
|
-
provider,
|
|
194
174
|
stripeInvoiceId,
|
|
195
|
-
stripeEventId
|
|
175
|
+
stripeEventId,
|
|
176
|
+
session,
|
|
177
|
+
...rest
|
|
196
178
|
});
|
|
197
179
|
|
|
198
|
-
return {
|
|
199
|
-
amount : -amount,
|
|
200
|
-
balance : {
|
|
201
|
-
before,
|
|
202
|
-
after
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
|
|
206
180
|
};
|
|
207
181
|
|
|
208
|
-
export {
|
|
182
|
+
export { credit, debit };
|
package/dist/transactions.js
CHANGED
package/package.json
CHANGED
package/dist/chunk-RC5E56UB.js
DELETED
|
@@ -1,154 +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
|
-
}) => {
|
|
14
|
-
await db.create({
|
|
15
|
-
authenticated,
|
|
16
|
-
collection: "transactions",
|
|
17
|
-
data: {
|
|
18
|
-
userId,
|
|
19
|
-
billable: {
|
|
20
|
-
item: "ai"
|
|
21
|
-
},
|
|
22
|
-
category,
|
|
23
|
-
tags,
|
|
24
|
-
amount,
|
|
25
|
-
balance,
|
|
26
|
-
...provider && { provider },
|
|
27
|
-
...stripeInvoiceId && { stripeInvoiceId },
|
|
28
|
-
...stripeEventId && { stripeEventId }
|
|
29
|
-
}
|
|
30
|
-
});
|
|
31
|
-
};
|
|
32
|
-
var credit = async ({
|
|
33
|
-
db,
|
|
34
|
-
authenticated,
|
|
35
|
-
userId,
|
|
36
|
-
amount,
|
|
37
|
-
category,
|
|
38
|
-
tags = [],
|
|
39
|
-
stripeInvoiceId,
|
|
40
|
-
stripeEventId
|
|
41
|
-
}) => {
|
|
42
|
-
var _a, _b, _c;
|
|
43
|
-
if (!Number.isInteger(amount) || amount <= 0) {
|
|
44
|
-
throw new Error(`credit() requires a positive integer amount, got ${amount}`);
|
|
45
|
-
}
|
|
46
|
-
const result = await db.update({
|
|
47
|
-
authenticated,
|
|
48
|
-
collection: "users",
|
|
49
|
-
query: {
|
|
50
|
-
id: userId
|
|
51
|
-
},
|
|
52
|
-
data: {
|
|
53
|
-
$inc: {
|
|
54
|
-
"balance.ai": amount
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
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);
|
|
59
|
-
if (typeof after !== "number") {
|
|
60
|
-
throw new Error(`credit() could not read updated balance for user ${userId}`);
|
|
61
|
-
}
|
|
62
|
-
const before = after - amount;
|
|
63
|
-
await recordTransaction({
|
|
64
|
-
db,
|
|
65
|
-
authenticated,
|
|
66
|
-
userId,
|
|
67
|
-
category,
|
|
68
|
-
tags,
|
|
69
|
-
amount,
|
|
70
|
-
balance: {
|
|
71
|
-
before,
|
|
72
|
-
after
|
|
73
|
-
},
|
|
74
|
-
stripeInvoiceId,
|
|
75
|
-
stripeEventId
|
|
76
|
-
});
|
|
77
|
-
return {
|
|
78
|
-
balance: {
|
|
79
|
-
before,
|
|
80
|
-
after
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
};
|
|
84
|
-
var InsufficientCreditsError = class extends Error {
|
|
85
|
-
constructor(message = "Insufficient AI credits") {
|
|
86
|
-
super(message);
|
|
87
|
-
this.name = "InsufficientCreditsError";
|
|
88
|
-
this.status = 402;
|
|
89
|
-
}
|
|
90
|
-
};
|
|
91
|
-
var debit = async ({
|
|
92
|
-
db,
|
|
93
|
-
authenticated,
|
|
94
|
-
userId,
|
|
95
|
-
amount,
|
|
96
|
-
category,
|
|
97
|
-
tags = [],
|
|
98
|
-
provider,
|
|
99
|
-
stripeInvoiceId,
|
|
100
|
-
stripeEventId
|
|
101
|
-
}) => {
|
|
102
|
-
var _a, _b, _c;
|
|
103
|
-
if (!Number.isInteger(amount) || amount <= 0) {
|
|
104
|
-
throw new Error(`debit() requires a positive integer amount, got ${amount}`);
|
|
105
|
-
}
|
|
106
|
-
const result = await db.update({
|
|
107
|
-
authenticated,
|
|
108
|
-
collection: "users",
|
|
109
|
-
query: {
|
|
110
|
-
id: userId,
|
|
111
|
-
"balance.ai": {
|
|
112
|
-
$gte: amount
|
|
113
|
-
}
|
|
114
|
-
},
|
|
115
|
-
data: {
|
|
116
|
-
$inc: {
|
|
117
|
-
"balance.ai": -amount
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
});
|
|
121
|
-
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);
|
|
122
|
-
if (typeof after !== "number") {
|
|
123
|
-
throw new InsufficientCreditsError();
|
|
124
|
-
}
|
|
125
|
-
const before = after + amount;
|
|
126
|
-
await recordTransaction({
|
|
127
|
-
db,
|
|
128
|
-
authenticated,
|
|
129
|
-
userId,
|
|
130
|
-
category,
|
|
131
|
-
tags,
|
|
132
|
-
amount: -amount,
|
|
133
|
-
balance: {
|
|
134
|
-
before,
|
|
135
|
-
after
|
|
136
|
-
},
|
|
137
|
-
provider,
|
|
138
|
-
stripeInvoiceId,
|
|
139
|
-
stripeEventId
|
|
140
|
-
});
|
|
141
|
-
return {
|
|
142
|
-
amount: -amount,
|
|
143
|
-
balance: {
|
|
144
|
-
before,
|
|
145
|
-
after
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
export {
|
|
151
|
-
credit,
|
|
152
|
-
InsufficientCreditsError,
|
|
153
|
-
debit
|
|
154
|
-
};
|