@nordsym/apiclaw 1.7.3 → 1.7.5

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.
Files changed (219) hide show
  1. package/convex/_generated/api.d.ts +115 -0
  2. package/convex/_generated/api.js +23 -0
  3. package/convex/_generated/dataModel.d.ts +60 -0
  4. package/convex/_generated/server.d.ts +143 -0
  5. package/convex/_generated/server.js +93 -0
  6. package/convex/adminActivate.d.ts +3 -0
  7. package/convex/adminActivate.d.ts.map +1 -0
  8. package/convex/adminActivate.js +47 -0
  9. package/convex/adminActivate.js.map +1 -0
  10. package/convex/adminActivate.ts +54 -0
  11. package/convex/adminStats.d.ts +3 -0
  12. package/convex/adminStats.d.ts.map +1 -0
  13. package/convex/adminStats.js +42 -0
  14. package/convex/adminStats.js.map +1 -0
  15. package/convex/adminStats.ts +44 -0
  16. package/convex/agents.d.ts +76 -0
  17. package/convex/agents.d.ts.map +1 -0
  18. package/convex/agents.js +699 -0
  19. package/convex/agents.js.map +1 -0
  20. package/convex/agents.ts +814 -0
  21. package/convex/analytics.d.ts +5 -0
  22. package/convex/analytics.d.ts.map +1 -0
  23. package/convex/analytics.js +166 -0
  24. package/convex/analytics.js.map +1 -0
  25. package/convex/analytics.ts +186 -0
  26. package/convex/billing.d.ts +88 -0
  27. package/convex/billing.d.ts.map +1 -0
  28. package/convex/billing.js +655 -0
  29. package/convex/billing.js.map +1 -0
  30. package/convex/billing.ts +791 -0
  31. package/convex/capabilities.d.ts +9 -0
  32. package/convex/capabilities.d.ts.map +1 -0
  33. package/convex/capabilities.js +145 -0
  34. package/convex/capabilities.js.map +1 -0
  35. package/convex/capabilities.ts +157 -0
  36. package/convex/chains.d.ts +68 -0
  37. package/convex/chains.d.ts.map +1 -0
  38. package/convex/chains.js +1105 -0
  39. package/convex/chains.js.map +1 -0
  40. package/convex/chains.ts +1318 -0
  41. package/convex/credits.d.ts +25 -0
  42. package/convex/credits.d.ts.map +1 -0
  43. package/convex/credits.js +186 -0
  44. package/convex/credits.js.map +1 -0
  45. package/convex/credits.ts +211 -0
  46. package/convex/crons.d.ts +3 -0
  47. package/convex/crons.d.ts.map +1 -0
  48. package/convex/crons.js +17 -0
  49. package/convex/crons.js.map +1 -0
  50. package/convex/crons.ts +28 -0
  51. package/convex/directCall.d.ts +72 -0
  52. package/convex/directCall.d.ts.map +1 -0
  53. package/convex/directCall.js +627 -0
  54. package/convex/directCall.js.map +1 -0
  55. package/convex/directCall.ts +678 -0
  56. package/convex/earnProgress.d.ts +58 -0
  57. package/convex/earnProgress.d.ts.map +1 -0
  58. package/convex/earnProgress.js +649 -0
  59. package/convex/earnProgress.js.map +1 -0
  60. package/convex/earnProgress.ts +753 -0
  61. package/convex/email.d.ts +14 -0
  62. package/convex/email.d.ts.map +1 -0
  63. package/convex/email.js +300 -0
  64. package/convex/email.js.map +1 -0
  65. package/convex/email.ts +329 -0
  66. package/convex/feedback.d.ts +7 -0
  67. package/convex/feedback.d.ts.map +1 -0
  68. package/convex/feedback.js +227 -0
  69. package/convex/feedback.js.map +1 -0
  70. package/convex/feedback.ts +265 -0
  71. package/convex/http.d.ts +3 -0
  72. package/convex/http.d.ts.map +1 -0
  73. package/convex/http.js +1405 -0
  74. package/convex/http.js.map +1 -0
  75. package/convex/http.ts +1577 -0
  76. package/convex/inbound.d.ts +2 -0
  77. package/convex/inbound.d.ts.map +1 -0
  78. package/convex/inbound.js +32 -0
  79. package/convex/inbound.js.map +1 -0
  80. package/convex/inbound.ts +32 -0
  81. package/convex/logs.d.ts +48 -0
  82. package/convex/logs.d.ts.map +1 -0
  83. package/convex/logs.js +592 -0
  84. package/convex/logs.js.map +1 -0
  85. package/convex/logs.ts +662 -0
  86. package/convex/mou.d.ts +6 -0
  87. package/convex/mou.d.ts.map +1 -0
  88. package/convex/mou.js +82 -0
  89. package/convex/mou.js.map +1 -0
  90. package/convex/mou.ts +91 -0
  91. package/convex/providerKeys.d.ts +31 -0
  92. package/convex/providerKeys.d.ts.map +1 -0
  93. package/convex/providerKeys.js +257 -0
  94. package/convex/providerKeys.js.map +1 -0
  95. package/convex/providerKeys.ts +289 -0
  96. package/convex/providers.d.ts +32 -0
  97. package/convex/providers.d.ts.map +1 -0
  98. package/convex/providers.js +814 -0
  99. package/convex/providers.js.map +1 -0
  100. package/convex/providers.ts +909 -0
  101. package/convex/purchases.d.ts +7 -0
  102. package/convex/purchases.d.ts.map +1 -0
  103. package/convex/purchases.js +157 -0
  104. package/convex/purchases.js.map +1 -0
  105. package/convex/purchases.ts +183 -0
  106. package/convex/ratelimit.d.ts +4 -0
  107. package/convex/ratelimit.d.ts.map +1 -0
  108. package/convex/ratelimit.js +91 -0
  109. package/convex/ratelimit.js.map +1 -0
  110. package/convex/ratelimit.ts +104 -0
  111. package/convex/schema.ts +805 -0
  112. package/convex/searchLogs.d.ts +4 -0
  113. package/convex/searchLogs.d.ts.map +1 -0
  114. package/convex/searchLogs.js +129 -0
  115. package/convex/searchLogs.js.map +1 -0
  116. package/convex/searchLogs.ts +146 -0
  117. package/convex/seedAPILayerAPIs.d.ts +7 -0
  118. package/convex/seedAPILayerAPIs.d.ts.map +1 -0
  119. package/convex/seedAPILayerAPIs.js +177 -0
  120. package/convex/seedAPILayerAPIs.js.map +1 -0
  121. package/convex/seedAPILayerAPIs.ts +191 -0
  122. package/convex/seedDirectCallConfigs.d.ts +2 -0
  123. package/convex/seedDirectCallConfigs.d.ts.map +1 -0
  124. package/convex/seedDirectCallConfigs.js +324 -0
  125. package/convex/seedDirectCallConfigs.js.map +1 -0
  126. package/convex/seedDirectCallConfigs.ts +336 -0
  127. package/convex/seedPratham.d.ts +6 -0
  128. package/convex/seedPratham.d.ts.map +1 -0
  129. package/convex/seedPratham.js +150 -0
  130. package/convex/seedPratham.js.map +1 -0
  131. package/convex/seedPratham.ts +161 -0
  132. package/convex/spendAlerts.d.ts +36 -0
  133. package/convex/spendAlerts.d.ts.map +1 -0
  134. package/convex/spendAlerts.js +380 -0
  135. package/convex/spendAlerts.js.map +1 -0
  136. package/convex/spendAlerts.ts +442 -0
  137. package/convex/stripeActions.d.ts +19 -0
  138. package/convex/stripeActions.d.ts.map +1 -0
  139. package/convex/stripeActions.js +411 -0
  140. package/convex/stripeActions.js.map +1 -0
  141. package/convex/stripeActions.ts +512 -0
  142. package/convex/teams.d.ts +21 -0
  143. package/convex/teams.d.ts.map +1 -0
  144. package/convex/teams.js +215 -0
  145. package/convex/teams.js.map +1 -0
  146. package/convex/teams.ts +243 -0
  147. package/convex/telemetry.d.ts +4 -0
  148. package/convex/telemetry.d.ts.map +1 -0
  149. package/convex/telemetry.js +74 -0
  150. package/convex/telemetry.js.map +1 -0
  151. package/convex/telemetry.ts +81 -0
  152. package/convex/tsconfig.json +25 -0
  153. package/convex/updateAPIStatus.d.ts +6 -0
  154. package/convex/updateAPIStatus.d.ts.map +1 -0
  155. package/convex/updateAPIStatus.js +40 -0
  156. package/convex/updateAPIStatus.js.map +1 -0
  157. package/convex/updateAPIStatus.ts +45 -0
  158. package/convex/usage.d.ts +27 -0
  159. package/convex/usage.d.ts.map +1 -0
  160. package/convex/usage.js +229 -0
  161. package/convex/usage.js.map +1 -0
  162. package/convex/usage.ts +260 -0
  163. package/convex/waitlist.d.ts +4 -0
  164. package/convex/waitlist.d.ts.map +1 -0
  165. package/convex/waitlist.js +49 -0
  166. package/convex/waitlist.js.map +1 -0
  167. package/convex/waitlist.ts +55 -0
  168. package/convex/webhooks.d.ts +12 -0
  169. package/convex/webhooks.d.ts.map +1 -0
  170. package/convex/webhooks.js +410 -0
  171. package/convex/webhooks.js.map +1 -0
  172. package/convex/webhooks.ts +494 -0
  173. package/convex/workspaces.d.ts +31 -0
  174. package/convex/workspaces.d.ts.map +1 -0
  175. package/convex/workspaces.js +975 -0
  176. package/convex/workspaces.js.map +1 -0
  177. package/convex/workspaces.ts +1130 -0
  178. package/dist/bin.js +0 -0
  179. package/dist/index.js +9 -0
  180. package/dist/index.js.map +1 -1
  181. package/package.json +1 -1
  182. package/src/index.ts +10 -0
  183. package/dist/chain-types.d.ts +0 -187
  184. package/dist/chain-types.d.ts.map +0 -1
  185. package/dist/chain-types.js +0 -33
  186. package/dist/chain-types.js.map +0 -1
  187. package/dist/registry/apis.json.bak +0 -248811
  188. package/dist/src/bin.js +0 -17
  189. package/dist/src/capability-router.js +0 -240
  190. package/dist/src/chainExecutor.js +0 -451
  191. package/dist/src/chainResolver.js +0 -518
  192. package/dist/src/cli/commands/doctor.js +0 -324
  193. package/dist/src/cli/commands/mcp-install.js +0 -255
  194. package/dist/src/cli/commands/restore.js +0 -259
  195. package/dist/src/cli/commands/setup.js +0 -205
  196. package/dist/src/cli/commands/uninstall.js +0 -188
  197. package/dist/src/cli/index.js +0 -111
  198. package/dist/src/cli.js +0 -302
  199. package/dist/src/confirmation.js +0 -240
  200. package/dist/src/credentials.js +0 -357
  201. package/dist/src/credits.js +0 -260
  202. package/dist/src/crypto.js +0 -66
  203. package/dist/src/discovery.js +0 -504
  204. package/dist/src/enterprise/env.js +0 -123
  205. package/dist/src/enterprise/script-generator.js +0 -460
  206. package/dist/src/execute-dynamic.js +0 -473
  207. package/dist/src/execute.js +0 -1727
  208. package/dist/src/index.js +0 -2062
  209. package/dist/src/metered.js +0 -80
  210. package/dist/src/open-apis.js +0 -276
  211. package/dist/src/proxy.js +0 -28
  212. package/dist/src/session.js +0 -86
  213. package/dist/src/stripe.js +0 -407
  214. package/dist/src/telemetry.js +0 -49
  215. package/dist/src/types.js +0 -2
  216. package/dist/src/utils/backup.js +0 -181
  217. package/dist/src/utils/config.js +0 -220
  218. package/dist/src/utils/os.js +0 -105
  219. package/dist/src/utils/paths.js +0 -159
@@ -0,0 +1,791 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query, internalMutation, internalAction, internalQuery } from "./_generated/server";
3
+ import { Id } from "./_generated/dataModel";
4
+ import { internal } from "./_generated/api";
5
+ import Stripe from "stripe";
6
+
7
+ // Initialize Stripe
8
+ function getStripe(): Stripe {
9
+ const key = process.env.STRIPE_SECRET_KEY;
10
+ if (!key) {
11
+ throw new Error("STRIPE_SECRET_KEY not configured");
12
+ }
13
+ return new Stripe(key);
14
+ }
15
+
16
+ // ============================================
17
+ // MUTATIONS
18
+ // ============================================
19
+
20
+ /**
21
+ * Link a Stripe customer to a workspace
22
+ */
23
+ export const linkCustomer = mutation({
24
+ args: {
25
+ workspaceId: v.id("workspaces"),
26
+ stripeCustomerId: v.string(),
27
+ },
28
+ handler: async (ctx, args) => {
29
+ const workspace = await ctx.db.get(args.workspaceId);
30
+ if (!workspace) {
31
+ throw new Error("Workspace not found");
32
+ }
33
+
34
+ await ctx.db.patch(args.workspaceId, {
35
+ stripeCustomerId: args.stripeCustomerId,
36
+ updatedAt: Date.now(),
37
+ });
38
+
39
+ return { success: true };
40
+ },
41
+ });
42
+
43
+ /**
44
+ * Update subscription status for a workspace
45
+ */
46
+ export const updateSubscription = mutation({
47
+ args: {
48
+ workspaceId: v.id("workspaces"),
49
+ stripeSubscriptionId: v.optional(v.string()),
50
+ billingPlan: v.string(),
51
+ },
52
+ handler: async (ctx, args) => {
53
+ const workspace = await ctx.db.get(args.workspaceId);
54
+ if (!workspace) {
55
+ throw new Error("Workspace not found");
56
+ }
57
+
58
+ // Update tier and usage limit based on plan
59
+ const planLimits: Record<string, number> = {
60
+ free: 50,
61
+ usage_based: 999999999, // Effectively unlimited
62
+ starter: 1000,
63
+ pro: 10000,
64
+ scale: 100000,
65
+ backer: 999999999, // Founding Backer - unlimited until end of 2026
66
+ };
67
+
68
+ const newLimit = planLimits[args.billingPlan] || 50;
69
+
70
+ // Determine tier
71
+ let newTier = "free";
72
+ if (args.billingPlan === "backer") {
73
+ newTier = "backer";
74
+ } else if (args.billingPlan !== "free") {
75
+ newTier = "pro";
76
+ }
77
+
78
+ // For Backer: set expiry to end of 2026
79
+ const backerUntil = args.billingPlan === "backer"
80
+ ? new Date("2026-12-31T23:59:59Z").getTime()
81
+ : undefined;
82
+
83
+ await ctx.db.patch(args.workspaceId, {
84
+ stripeSubscriptionId: args.stripeSubscriptionId,
85
+ billingPlan: args.billingPlan,
86
+ tier: newTier,
87
+ usageLimit: newLimit,
88
+ ...(backerUntil && { backerUntil }),
89
+ updatedAt: Date.now(),
90
+ });
91
+
92
+ return { success: true, newLimit };
93
+ },
94
+ });
95
+
96
+ /**
97
+ * Record daily usage for billing
98
+ */
99
+ export const recordUsage = mutation({
100
+ args: {
101
+ workspaceId: v.id("workspaces"),
102
+ callCount: v.number(),
103
+ date: v.string(), // "2026-02-28" format
104
+ },
105
+ handler: async (ctx, args) => {
106
+ // Check if record exists for this date
107
+ const existing = await ctx.db
108
+ .query("usageRecords")
109
+ .withIndex("by_workspaceId_date", (q) =>
110
+ q.eq("workspaceId", args.workspaceId).eq("date", args.date)
111
+ )
112
+ .unique();
113
+
114
+ if (existing) {
115
+ // Update existing record
116
+ await ctx.db.patch(existing._id, {
117
+ callCount: existing.callCount + args.callCount,
118
+ updatedAt: Date.now(),
119
+ });
120
+ return { id: existing._id, callCount: existing.callCount + args.callCount };
121
+ } else {
122
+ // Create new record
123
+ const id = await ctx.db.insert("usageRecords", {
124
+ workspaceId: args.workspaceId,
125
+ date: args.date,
126
+ callCount: args.callCount,
127
+ reportedToStripe: false,
128
+ createdAt: Date.now(),
129
+ updatedAt: Date.now(),
130
+ });
131
+ return { id, callCount: args.callCount };
132
+ }
133
+ },
134
+ });
135
+
136
+ /**
137
+ * Process a successful payment (from webhook)
138
+ */
139
+ export const processPayment = mutation({
140
+ args: {
141
+ stripeInvoiceId: v.string(),
142
+ workspaceId: v.id("workspaces"),
143
+ amount: v.number(), // in cents
144
+ periodStart: v.number(),
145
+ periodEnd: v.number(),
146
+ callCount: v.number(),
147
+ pdfUrl: v.optional(v.string()),
148
+ },
149
+ handler: async (ctx, args) => {
150
+ // Check for idempotency - don't process same invoice twice
151
+ const existing = await ctx.db
152
+ .query("invoices")
153
+ .withIndex("by_stripeInvoiceId", (q) =>
154
+ q.eq("stripeInvoiceId", args.stripeInvoiceId)
155
+ )
156
+ .unique();
157
+
158
+ if (existing) {
159
+ // Already processed, return existing
160
+ return { id: existing._id, alreadyProcessed: true };
161
+ }
162
+
163
+ // Create invoice record
164
+ const id = await ctx.db.insert("invoices", {
165
+ workspaceId: args.workspaceId,
166
+ stripeInvoiceId: args.stripeInvoiceId,
167
+ amount: args.amount,
168
+ status: "paid",
169
+ periodStart: args.periodStart,
170
+ periodEnd: args.periodEnd,
171
+ callCount: args.callCount,
172
+ pdfUrl: args.pdfUrl,
173
+ createdAt: Date.now(),
174
+ });
175
+
176
+ // Update workspace last billing date
177
+ await ctx.db.patch(args.workspaceId, {
178
+ lastBillingDate: Date.now(),
179
+ updatedAt: Date.now(),
180
+ });
181
+
182
+ return { id, alreadyProcessed: false };
183
+ },
184
+ });
185
+
186
+ /**
187
+ * Increment credit balance (for prepaid credits)
188
+ */
189
+ export const incrementCredits = mutation({
190
+ args: {
191
+ workspaceId: v.id("workspaces"),
192
+ amount: v.number(), // in cents
193
+ },
194
+ handler: async (ctx, args) => {
195
+ const workspace = await ctx.db.get(args.workspaceId);
196
+ if (!workspace) {
197
+ throw new Error("Workspace not found");
198
+ }
199
+
200
+ const currentBalance = workspace.creditBalance || 0;
201
+ const newBalance = currentBalance + args.amount;
202
+
203
+ await ctx.db.patch(args.workspaceId, {
204
+ creditBalance: newBalance,
205
+ updatedAt: Date.now(),
206
+ });
207
+
208
+ return { previousBalance: currentBalance, newBalance };
209
+ },
210
+ });
211
+
212
+ /**
213
+ * Decrement credit balance (when using prepaid credits)
214
+ */
215
+ export const decrementCredits = mutation({
216
+ args: {
217
+ workspaceId: v.id("workspaces"),
218
+ amount: v.number(), // in cents
219
+ },
220
+ handler: async (ctx, args) => {
221
+ const workspace = await ctx.db.get(args.workspaceId);
222
+ if (!workspace) {
223
+ throw new Error("Workspace not found");
224
+ }
225
+
226
+ const currentBalance = workspace.creditBalance || 0;
227
+ if (currentBalance < args.amount) {
228
+ throw new Error("Insufficient credit balance");
229
+ }
230
+
231
+ const newBalance = currentBalance - args.amount;
232
+
233
+ await ctx.db.patch(args.workspaceId, {
234
+ creditBalance: newBalance,
235
+ updatedAt: Date.now(),
236
+ });
237
+
238
+ return { previousBalance: currentBalance, newBalance };
239
+ },
240
+ });
241
+
242
+ /**
243
+ * Mark usage as reported to Stripe
244
+ */
245
+ export const markUsageReported = internalMutation({
246
+ args: {
247
+ usageRecordId: v.id("usageRecords"),
248
+ stripeUsageRecordId: v.string(),
249
+ },
250
+ handler: async (ctx, args) => {
251
+ await ctx.db.patch(args.usageRecordId, {
252
+ reportedToStripe: true,
253
+ stripeUsageRecordId: args.stripeUsageRecordId,
254
+ updatedAt: Date.now(),
255
+ });
256
+ },
257
+ });
258
+
259
+ /**
260
+ * Update invoice status (from webhook)
261
+ */
262
+ export const updateInvoiceStatus = mutation({
263
+ args: {
264
+ stripeInvoiceId: v.string(),
265
+ status: v.string(),
266
+ },
267
+ handler: async (ctx, args) => {
268
+ const invoice = await ctx.db
269
+ .query("invoices")
270
+ .withIndex("by_stripeInvoiceId", (q) =>
271
+ q.eq("stripeInvoiceId", args.stripeInvoiceId)
272
+ )
273
+ .unique();
274
+
275
+ if (!invoice) {
276
+ return { found: false };
277
+ }
278
+
279
+ await ctx.db.patch(invoice._id, {
280
+ status: args.status,
281
+ });
282
+
283
+ return { found: true, id: invoice._id };
284
+ },
285
+ });
286
+
287
+ /**
288
+ * Reset usage count on subscription cancellation
289
+ * Gives user a clean slate when downgrading to free
290
+ */
291
+ export const resetUsageOnCancellation = mutation({
292
+ args: {
293
+ workspaceId: v.id("workspaces"),
294
+ },
295
+ handler: async (ctx, args) => {
296
+ const workspace = await ctx.db.get(args.workspaceId);
297
+ if (!workspace) {
298
+ throw new Error("Workspace not found");
299
+ }
300
+
301
+ await ctx.db.patch(args.workspaceId, {
302
+ usageCount: 0,
303
+ updatedAt: Date.now(),
304
+ });
305
+
306
+ return { success: true, previousUsage: workspace.usageCount };
307
+ },
308
+ });
309
+
310
+ /**
311
+ * Update payment method info (from webhook)
312
+ */
313
+ export const updatePaymentMethodInfo = mutation({
314
+ args: {
315
+ workspaceId: v.id("workspaces"),
316
+ hasPaymentMethod: v.boolean(),
317
+ paymentMethodType: v.optional(v.string()),
318
+ cardBrand: v.optional(v.string()),
319
+ cardLast4: v.optional(v.string()),
320
+ },
321
+ handler: async (ctx, args) => {
322
+ const workspace = await ctx.db.get(args.workspaceId);
323
+ if (!workspace) {
324
+ throw new Error("Workspace not found");
325
+ }
326
+
327
+ await ctx.db.patch(args.workspaceId, {
328
+ hasPaymentMethod: args.hasPaymentMethod,
329
+ paymentMethodType: args.paymentMethodType,
330
+ cardBrand: args.cardBrand,
331
+ cardLast4: args.cardLast4,
332
+ updatedAt: Date.now(),
333
+ });
334
+
335
+ return { success: true };
336
+ },
337
+ });
338
+
339
+ // ============================================
340
+ // QUERIES
341
+ // ============================================
342
+
343
+ /**
344
+ * Get billing info for a workspace
345
+ */
346
+ export const getInfo = query({
347
+ args: {
348
+ workspaceId: v.id("workspaces"),
349
+ },
350
+ handler: async (ctx, args) => {
351
+ const workspace = await ctx.db.get(args.workspaceId);
352
+ if (!workspace) {
353
+ throw new Error("Workspace not found");
354
+ }
355
+
356
+ // Get recent invoices
357
+ const invoices = await ctx.db
358
+ .query("invoices")
359
+ .withIndex("by_workspaceId_createdAt", (q) =>
360
+ q.eq("workspaceId", args.workspaceId)
361
+ )
362
+ .order("desc")
363
+ .take(10);
364
+
365
+ // Calculate current period usage
366
+ const now = new Date();
367
+ const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
368
+ const periodStartStr = periodStart.toISOString().split("T")[0];
369
+
370
+ const usageRecords = await ctx.db
371
+ .query("usageRecords")
372
+ .withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId))
373
+ .collect();
374
+
375
+ const currentPeriodUsage = usageRecords
376
+ .filter((r) => r.date >= periodStartStr)
377
+ .reduce((sum, r) => sum + r.callCount, 0);
378
+
379
+ // Determine plan limits
380
+ const plan = workspace.billingPlan || "free";
381
+ const planLimits: Record<string, number> = {
382
+ free: 100,
383
+ usage_based: 999999999,
384
+ starter: 1000,
385
+ pro: 10000,
386
+ scale: 100000,
387
+ };
388
+
389
+ return {
390
+ plan,
391
+ tier: workspace.tier,
392
+ usage: workspace.usageCount,
393
+ currentPeriodUsage,
394
+ limit: workspace.usageLimit,
395
+ creditBalance: workspace.creditBalance || 0,
396
+ stripeCustomerId: workspace.stripeCustomerId,
397
+ stripeSubscriptionId: workspace.stripeSubscriptionId,
398
+ lastBillingDate: workspace.lastBillingDate,
399
+ invoices: invoices.map((inv) => ({
400
+ id: inv._id,
401
+ stripeInvoiceId: inv.stripeInvoiceId,
402
+ amount: inv.amount,
403
+ status: inv.status,
404
+ periodStart: inv.periodStart,
405
+ periodEnd: inv.periodEnd,
406
+ callCount: inv.callCount,
407
+ pdfUrl: inv.pdfUrl,
408
+ createdAt: inv.createdAt,
409
+ })),
410
+ // Check if payment method needed
411
+ needsPaymentMethod:
412
+ plan === "free" && workspace.usageCount >= workspace.usageLimit,
413
+ };
414
+ },
415
+ });
416
+
417
+ /**
418
+ * Get current period usage
419
+ */
420
+ export const getCurrentUsage = query({
421
+ args: {
422
+ workspaceId: v.id("workspaces"),
423
+ },
424
+ handler: async (ctx, args) => {
425
+ const workspace = await ctx.db.get(args.workspaceId);
426
+ if (!workspace) {
427
+ throw new Error("Workspace not found");
428
+ }
429
+
430
+ // Current billing period (month)
431
+ const now = new Date();
432
+ const periodStart = new Date(now.getFullYear(), now.getMonth(), 1);
433
+ const periodEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0);
434
+ const periodStartStr = periodStart.toISOString().split("T")[0];
435
+
436
+ // Get usage records for this period
437
+ const usageRecords = await ctx.db
438
+ .query("usageRecords")
439
+ .withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId))
440
+ .collect();
441
+
442
+ const periodRecords = usageRecords.filter((r) => r.date >= periodStartStr);
443
+ const callCount = periodRecords.reduce((sum, r) => sum + r.callCount, 0);
444
+
445
+ // Calculate estimated cost (for usage-based billing)
446
+ const FREE_CALLS = 100;
447
+ const COST_PER_CALL = 1; // 1 cent = $0.01
448
+ const billableCalls = Math.max(0, callCount - FREE_CALLS);
449
+ const estimatedCost = billableCalls * COST_PER_CALL;
450
+
451
+ return {
452
+ callCount,
453
+ periodStart: periodStart.getTime(),
454
+ periodEnd: periodEnd.getTime(),
455
+ limit: workspace.usageLimit,
456
+ remaining: Math.max(0, workspace.usageLimit - workspace.usageCount),
457
+ percentUsed: Math.round((workspace.usageCount / workspace.usageLimit) * 100),
458
+ billableCalls,
459
+ estimatedCostCents: estimatedCost,
460
+ dailyBreakdown: periodRecords.map((r) => ({
461
+ date: r.date,
462
+ calls: r.callCount,
463
+ reportedToStripe: r.reportedToStripe,
464
+ })),
465
+ };
466
+ },
467
+ });
468
+
469
+ /**
470
+ * Get invoices for a workspace
471
+ */
472
+ export const getInvoices = query({
473
+ args: {
474
+ workspaceId: v.id("workspaces"),
475
+ limit: v.optional(v.number()),
476
+ },
477
+ handler: async (ctx, args) => {
478
+ const limit = args.limit || 20;
479
+
480
+ const invoices = await ctx.db
481
+ .query("invoices")
482
+ .withIndex("by_workspaceId_createdAt", (q) =>
483
+ q.eq("workspaceId", args.workspaceId)
484
+ )
485
+ .order("desc")
486
+ .take(limit);
487
+
488
+ return invoices.map((inv) => ({
489
+ id: inv._id,
490
+ stripeInvoiceId: inv.stripeInvoiceId,
491
+ amount: inv.amount,
492
+ amountFormatted: `$${(inv.amount / 100).toFixed(2)}`,
493
+ status: inv.status,
494
+ periodStart: inv.periodStart,
495
+ periodEnd: inv.periodEnd,
496
+ callCount: inv.callCount,
497
+ pdfUrl: inv.pdfUrl,
498
+ createdAt: inv.createdAt,
499
+ }));
500
+ },
501
+ });
502
+
503
+ /**
504
+ * Get unreported usage records (for cron job)
505
+ */
506
+ export const getUnreportedUsage = query({
507
+ args: {},
508
+ handler: async (ctx) => {
509
+ const unreported = await ctx.db
510
+ .query("usageRecords")
511
+ .withIndex("by_reportedToStripe", (q) => q.eq("reportedToStripe", false))
512
+ .collect();
513
+
514
+ return unreported;
515
+ },
516
+ });
517
+
518
+ /**
519
+ * Get workspace by Stripe customer ID
520
+ */
521
+ export const getByStripeCustomerId = query({
522
+ args: {
523
+ stripeCustomerId: v.string(),
524
+ },
525
+ handler: async (ctx, args) => {
526
+ return await ctx.db
527
+ .query("workspaces")
528
+ .withIndex("by_stripeCustomerId", (q) =>
529
+ q.eq("stripeCustomerId", args.stripeCustomerId)
530
+ )
531
+ .unique();
532
+ },
533
+ });
534
+
535
+ /**
536
+ * Get workspace by ID
537
+ */
538
+ export const getWorkspace = query({
539
+ args: {
540
+ id: v.id("workspaces"),
541
+ },
542
+ handler: async (ctx, args) => {
543
+ return await ctx.db.get(args.id);
544
+ },
545
+ });
546
+
547
+ // ============================================
548
+ // STRIPE USAGE REPORTING (Internal Actions)
549
+ // ============================================
550
+
551
+ /**
552
+ * Get all workspaces with active Stripe subscriptions (internal)
553
+ */
554
+ export const getActiveSubscriptions = internalQuery({
555
+ args: {},
556
+ handler: async (ctx) => {
557
+ // Get all workspaces with a subscription ID
558
+ const workspaces = await ctx.db
559
+ .query("workspaces")
560
+ .filter((q) => q.neq(q.field("stripeSubscriptionId"), undefined))
561
+ .collect();
562
+
563
+ // Filter to only those with billing plan that requires reporting
564
+ return workspaces.filter(
565
+ (w) => w.billingPlan === "usage_based" && w.stripeSubscriptionId
566
+ );
567
+ },
568
+ });
569
+
570
+ /**
571
+ * Get unreported usage records for a specific workspace (internal)
572
+ */
573
+ export const getUnreportedUsageForWorkspace = internalQuery({
574
+ args: {
575
+ workspaceId: v.id("workspaces"),
576
+ },
577
+ handler: async (ctx, args) => {
578
+ return await ctx.db
579
+ .query("usageRecords")
580
+ .withIndex("by_workspaceId", (q) => q.eq("workspaceId", args.workspaceId))
581
+ .filter((q) => q.eq(q.field("reportedToStripe"), false))
582
+ .collect();
583
+ },
584
+ });
585
+
586
+ /**
587
+ * Mark multiple usage records as reported (internal)
588
+ */
589
+ export const markUsageRecordsReported = internalMutation({
590
+ args: {
591
+ usageRecordIds: v.array(v.id("usageRecords")),
592
+ stripeUsageRecordId: v.string(),
593
+ },
594
+ handler: async (ctx, args) => {
595
+ const now = Date.now();
596
+ for (const recordId of args.usageRecordIds) {
597
+ await ctx.db.patch(recordId, {
598
+ reportedToStripe: true,
599
+ stripeUsageRecordId: args.stripeUsageRecordId,
600
+ updatedAt: now,
601
+ });
602
+ }
603
+ },
604
+ });
605
+
606
+ /**
607
+ * Report usage to Stripe for a single workspace
608
+ * Internal action - called by the daily cron
609
+ */
610
+ export const reportUsageToStripe = internalAction({
611
+ args: {
612
+ workspaceId: v.id("workspaces"),
613
+ stripeSubscriptionId: v.string(),
614
+ },
615
+ handler: async (ctx, args): Promise<{ success: boolean; callCount: number; error?: string }> => {
616
+ try {
617
+ // Get unreported usage records
618
+ const usageRecords = await ctx.runQuery(
619
+ internal.billing.getUnreportedUsageForWorkspace,
620
+ { workspaceId: args.workspaceId }
621
+ );
622
+
623
+ if (usageRecords.length === 0) {
624
+ return { success: true, callCount: 0 };
625
+ }
626
+
627
+ // Sum up all unreported calls
628
+ const totalCalls = usageRecords.reduce(
629
+ (sum: number, r: { callCount: number }) => sum + r.callCount,
630
+ 0
631
+ );
632
+
633
+ if (totalCalls === 0) {
634
+ // Mark as reported even if 0 calls (to prevent re-processing)
635
+ await ctx.runMutation(internal.billing.markUsageRecordsReported, {
636
+ usageRecordIds: usageRecords.map((r: { _id: Id<"usageRecords"> }) => r._id),
637
+ stripeUsageRecordId: "zero_usage",
638
+ });
639
+ return { success: true, callCount: 0 };
640
+ }
641
+
642
+ const stripe = getStripe();
643
+
644
+ // Get subscription to find the metered subscription item
645
+ const subscription = await stripe.subscriptions.retrieve(args.stripeSubscriptionId);
646
+
647
+ // Find the metered price item (usage_based price)
648
+ const meteredItem = subscription.items.data.find((item) => {
649
+ return item.price.recurring?.usage_type === "metered";
650
+ });
651
+
652
+ if (!meteredItem) {
653
+ console.error(
654
+ `No metered subscription item found for subscription ${args.stripeSubscriptionId}`
655
+ );
656
+ return {
657
+ success: false,
658
+ callCount: totalCalls,
659
+ error: "No metered subscription item found",
660
+ };
661
+ }
662
+
663
+ // Report usage to Stripe using usage records API
664
+ // Using raw fetch since SDK method varies by version
665
+ const stripeKey = process.env.STRIPE_SECRET_KEY;
666
+ const usageRecordResponse = await fetch(
667
+ `https://api.stripe.com/v1/subscription_items/${meteredItem.id}/usage_records`,
668
+ {
669
+ method: "POST",
670
+ headers: {
671
+ "Authorization": `Bearer ${stripeKey}`,
672
+ "Content-Type": "application/x-www-form-urlencoded",
673
+ },
674
+ body: new URLSearchParams({
675
+ quantity: String(totalCalls),
676
+ timestamp: String(Math.floor(Date.now() / 1000)),
677
+ action: "increment",
678
+ }),
679
+ }
680
+ );
681
+
682
+ if (!usageRecordResponse.ok) {
683
+ const errorData = await usageRecordResponse.text();
684
+ throw new Error(`Stripe API error: ${usageRecordResponse.status} - ${errorData}`);
685
+ }
686
+
687
+ const usageRecord = await usageRecordResponse.json() as { id: string };
688
+
689
+ // Mark records as reported
690
+ await ctx.runMutation(internal.billing.markUsageRecordsReported, {
691
+ usageRecordIds: usageRecords.map((r: { _id: Id<"usageRecords"> }) => r._id),
692
+ stripeUsageRecordId: usageRecord.id,
693
+ });
694
+
695
+ console.log(
696
+ `Reported ${totalCalls} calls for workspace ${args.workspaceId} to Stripe`
697
+ );
698
+
699
+ return { success: true, callCount: totalCalls };
700
+ } catch (error: any) {
701
+ console.error(
702
+ `Failed to report usage for workspace ${args.workspaceId}:`,
703
+ error
704
+ );
705
+ return {
706
+ success: false,
707
+ callCount: 0,
708
+ error: error.message || "Unknown error",
709
+ };
710
+ }
711
+ },
712
+ });
713
+
714
+ // Type for workspace with active subscription
715
+ type ActiveWorkspace = {
716
+ _id: Id<"workspaces">;
717
+ email: string;
718
+ stripeSubscriptionId?: string;
719
+ };
720
+
721
+ // Type for cron results
722
+ type CronResults = {
723
+ total: number;
724
+ success: number;
725
+ failed: number;
726
+ skipped: number;
727
+ totalCallsReported: number;
728
+ errors: string[];
729
+ };
730
+
731
+ /**
732
+ * Daily cron job: Report all unreported usage to Stripe
733
+ * Runs at 00:05 UTC
734
+ */
735
+ export const reportAllUsageToStripe = internalAction({
736
+ args: {},
737
+ handler: async (ctx): Promise<CronResults> => {
738
+ console.log("[Cron] Starting daily usage reporting to Stripe...");
739
+
740
+ // Get all workspaces with active subscriptions
741
+ const workspaces: ActiveWorkspace[] = await ctx.runQuery(
742
+ internal.billing.getActiveSubscriptions,
743
+ {}
744
+ );
745
+
746
+ console.log(`[Cron] Found ${workspaces.length} workspaces with active subscriptions`);
747
+
748
+ const results: CronResults = {
749
+ total: workspaces.length,
750
+ success: 0,
751
+ failed: 0,
752
+ skipped: 0,
753
+ totalCallsReported: 0,
754
+ errors: [],
755
+ };
756
+
757
+ // Process each workspace
758
+ for (const workspace of workspaces) {
759
+ if (!workspace.stripeSubscriptionId) {
760
+ results.skipped++;
761
+ continue;
762
+ }
763
+
764
+ try {
765
+ const result = await ctx.runAction(internal.billing.reportUsageToStripe, {
766
+ workspaceId: workspace._id,
767
+ stripeSubscriptionId: workspace.stripeSubscriptionId,
768
+ });
769
+
770
+ if (result.success) {
771
+ results.success++;
772
+ results.totalCallsReported += result.callCount;
773
+ } else {
774
+ results.failed++;
775
+ results.errors.push(
776
+ `${workspace.email}: ${result.error || "Unknown error"}`
777
+ );
778
+ }
779
+ } catch (error: any) {
780
+ results.failed++;
781
+ results.errors.push(`${workspace.email}: ${error.message || "Unknown error"}`);
782
+ console.error(`[Cron] Error processing workspace ${workspace._id}:`, error);
783
+ // Continue with next workspace
784
+ }
785
+ }
786
+
787
+ console.log("[Cron] Daily usage reporting complete:", JSON.stringify(results));
788
+
789
+ return results;
790
+ },
791
+ });