@goscribe/server 1.6.0 → 1.7.0

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 (281) hide show
  1. package/dist/generated/prisma/client.d.ts +224 -0
  2. package/dist/generated/prisma/client.js +34 -0
  3. package/dist/generated/prisma/commonInputTypes.d.ts +941 -0
  4. package/dist/generated/prisma/commonInputTypes.js +10 -0
  5. package/dist/generated/prisma/enums.d.ts +67 -0
  6. package/dist/generated/prisma/enums.js +66 -0
  7. package/dist/generated/prisma/internal/class.d.ts +539 -0
  8. package/dist/generated/prisma/internal/class.js +49 -0
  9. package/dist/generated/prisma/internal/prismaNamespace.d.ts +3924 -0
  10. package/dist/generated/prisma/internal/prismaNamespace.js +557 -0
  11. package/dist/generated/prisma/models/ActivityLog.d.ts +1847 -0
  12. package/dist/generated/prisma/models/ActivityLog.js +1 -0
  13. package/dist/generated/prisma/models/Artifact.d.ts +2345 -0
  14. package/dist/generated/prisma/models/Artifact.js +1 -0
  15. package/dist/generated/prisma/models/ArtifactVersion.d.ts +1550 -0
  16. package/dist/generated/prisma/models/ArtifactVersion.js +1 -0
  17. package/dist/generated/prisma/models/Channel.d.ts +1257 -0
  18. package/dist/generated/prisma/models/Channel.js +1 -0
  19. package/dist/generated/prisma/models/Chat.d.ts +1339 -0
  20. package/dist/generated/prisma/models/Chat.js +1 -0
  21. package/dist/generated/prisma/models/CopilotConversation.d.ts +1450 -0
  22. package/dist/generated/prisma/models/CopilotConversation.js +1 -0
  23. package/dist/generated/prisma/models/CopilotMessage.d.ts +1179 -0
  24. package/dist/generated/prisma/models/CopilotMessage.js +1 -0
  25. package/dist/generated/prisma/models/FileAsset.d.ts +1832 -0
  26. package/dist/generated/prisma/models/FileAsset.js +1 -0
  27. package/dist/generated/prisma/models/Flashcard.d.ts +1460 -0
  28. package/dist/generated/prisma/models/Flashcard.js +1 -0
  29. package/dist/generated/prisma/models/FlashcardProgress.d.ts +1782 -0
  30. package/dist/generated/prisma/models/FlashcardProgress.js +1 -0
  31. package/dist/generated/prisma/models/Folder.d.ts +1685 -0
  32. package/dist/generated/prisma/models/Folder.js +1 -0
  33. package/dist/generated/prisma/models/IdempotencyRecord.d.ts +1319 -0
  34. package/dist/generated/prisma/models/IdempotencyRecord.js +1 -0
  35. package/dist/generated/prisma/models/Invoice.d.ts +1586 -0
  36. package/dist/generated/prisma/models/Invoice.js +1 -0
  37. package/dist/generated/prisma/models/KnowledgeBase.d.ts +1721 -0
  38. package/dist/generated/prisma/models/KnowledgeBase.js +1 -0
  39. package/dist/generated/prisma/models/KnowledgeBaseChunk.d.ts +1333 -0
  40. package/dist/generated/prisma/models/KnowledgeBaseChunk.js +1 -0
  41. package/dist/generated/prisma/models/KnowledgeBaseDocument.d.ts +1695 -0
  42. package/dist/generated/prisma/models/KnowledgeBaseDocument.js +1 -0
  43. package/dist/generated/prisma/models/Notification.d.ts +1992 -0
  44. package/dist/generated/prisma/models/Notification.js +1 -0
  45. package/dist/generated/prisma/models/PasswordResetToken.d.ts +1210 -0
  46. package/dist/generated/prisma/models/PasswordResetToken.js +1 -0
  47. package/dist/generated/prisma/models/Plan.d.ts +1431 -0
  48. package/dist/generated/prisma/models/Plan.js +1 -0
  49. package/dist/generated/prisma/models/PlanLimit.d.ts +1328 -0
  50. package/dist/generated/prisma/models/PlanLimit.js +1 -0
  51. package/dist/generated/prisma/models/PodcastSegment.d.ts +1564 -0
  52. package/dist/generated/prisma/models/PodcastSegment.js +1 -0
  53. package/dist/generated/prisma/models/ResourcePrice.d.ts +1008 -0
  54. package/dist/generated/prisma/models/ResourcePrice.js +1 -0
  55. package/dist/generated/prisma/models/Role.d.ts +1065 -0
  56. package/dist/generated/prisma/models/Role.js +1 -0
  57. package/dist/generated/prisma/models/Session.d.ts +1105 -0
  58. package/dist/generated/prisma/models/Session.js +1 -0
  59. package/dist/generated/prisma/models/StripeEvent.d.ts +1081 -0
  60. package/dist/generated/prisma/models/StripeEvent.js +1 -0
  61. package/dist/generated/prisma/models/StudyGuideComment.d.ts +1321 -0
  62. package/dist/generated/prisma/models/StudyGuideComment.js +1 -0
  63. package/dist/generated/prisma/models/StudyGuideHighlight.d.ts +1629 -0
  64. package/dist/generated/prisma/models/StudyGuideHighlight.js +1 -0
  65. package/dist/generated/prisma/models/Subscription.d.ts +1677 -0
  66. package/dist/generated/prisma/models/Subscription.js +1 -0
  67. package/dist/generated/prisma/models/User.d.ts +7559 -0
  68. package/dist/generated/prisma/models/User.js +1 -0
  69. package/dist/generated/prisma/models/UserCredit.d.ts +1249 -0
  70. package/dist/generated/prisma/models/UserCredit.js +1 -0
  71. package/dist/generated/prisma/models/VerificationToken.d.ts +946 -0
  72. package/dist/generated/prisma/models/VerificationToken.js +1 -0
  73. package/dist/generated/prisma/models/WorksheetPreset.d.ts +1433 -0
  74. package/dist/generated/prisma/models/WorksheetPreset.js +1 -0
  75. package/dist/generated/prisma/models/WorksheetQuestion.d.ts +1491 -0
  76. package/dist/generated/prisma/models/WorksheetQuestion.js +1 -0
  77. package/dist/generated/prisma/models/WorksheetQuestionProgress.d.ts +1620 -0
  78. package/dist/generated/prisma/models/WorksheetQuestionProgress.js +1 -0
  79. package/dist/generated/prisma/models/Workspace.d.ts +3620 -0
  80. package/dist/generated/prisma/models/Workspace.js +1 -0
  81. package/dist/generated/prisma/models/WorkspaceInvitation.d.ts +1490 -0
  82. package/dist/generated/prisma/models/WorkspaceInvitation.js +1 -0
  83. package/dist/generated/prisma/models/WorkspaceKnowledgeBase.d.ts +1410 -0
  84. package/dist/generated/prisma/models/WorkspaceKnowledgeBase.js +1 -0
  85. package/dist/generated/prisma/models/WorkspaceMember.d.ts +1326 -0
  86. package/dist/generated/prisma/models/WorkspaceMember.js +1 -0
  87. package/dist/generated/prisma/models.d.ts +39 -0
  88. package/dist/generated/prisma/models.js +1 -0
  89. package/dist/src/context.d.ts +27 -0
  90. package/dist/src/context.js +33 -0
  91. package/dist/src/index.d.ts +3 -0
  92. package/dist/src/index.js +1 -0
  93. package/dist/src/lib/ai/config.d.ts +20 -0
  94. package/dist/src/lib/ai/config.js +31 -0
  95. package/dist/src/lib/ai/embedding-client.d.ts +8 -0
  96. package/dist/src/lib/ai/embedding-client.js +30 -0
  97. package/dist/src/lib/ai/index.d.ts +48 -0
  98. package/dist/src/lib/ai/index.js +29 -0
  99. package/dist/src/lib/ai/inference-backend/client.d.ts +28 -0
  100. package/dist/src/lib/ai/inference-backend/client.js +301 -0
  101. package/dist/src/lib/ai/inference-backend/mocks.d.ts +12 -0
  102. package/dist/src/lib/ai/inference-backend/mocks.js +133 -0
  103. package/dist/src/lib/ai/inference-backend/types.d.ts +44 -0
  104. package/dist/src/lib/ai/inference-backend/types.js +1 -0
  105. package/dist/src/lib/ai/json-parse.d.ts +2 -0
  106. package/dist/src/lib/ai/json-parse.js +34 -0
  107. package/dist/src/lib/ai/llm-client.d.ts +7 -0
  108. package/dist/src/lib/ai/llm-client.js +36 -0
  109. package/dist/src/lib/ai/mock.d.ts +2 -0
  110. package/dist/src/lib/ai/mock.js +10 -0
  111. package/dist/src/lib/ai/types.d.ts +9 -0
  112. package/dist/src/lib/ai/types.js +1 -0
  113. package/dist/src/lib/auth.d.ts +36 -0
  114. package/dist/src/lib/auth.js +117 -0
  115. package/dist/src/lib/chunking.d.ts +19 -0
  116. package/dist/src/lib/chunking.js +47 -0
  117. package/dist/src/lib/constants.d.ts +13 -0
  118. package/dist/src/lib/constants.js +12 -0
  119. package/dist/src/lib/curated-kb-seed.d.ts +12 -0
  120. package/dist/src/lib/curated-kb-seed.js +155 -0
  121. package/dist/src/lib/email.d.ts +11 -0
  122. package/dist/src/lib/email.js +152 -0
  123. package/dist/src/lib/embeddings.d.ts +2 -0
  124. package/dist/src/lib/embeddings.js +1 -0
  125. package/dist/src/lib/ensure-curated-kb-catalog.d.ts +6 -0
  126. package/dist/src/lib/ensure-curated-kb-catalog.js +53 -0
  127. package/dist/src/lib/env.d.ts +41 -0
  128. package/dist/src/lib/env.js +57 -0
  129. package/dist/src/lib/errors.d.ts +33 -0
  130. package/dist/src/lib/errors.js +78 -0
  131. package/dist/src/lib/file.d.ts +0 -0
  132. package/dist/src/lib/file.js +1 -0
  133. package/dist/src/lib/inference.d.ts +1 -0
  134. package/dist/src/lib/inference.js +1 -0
  135. package/dist/src/lib/kb-meta.d.ts +8 -0
  136. package/dist/src/lib/kb-meta.js +77 -0
  137. package/dist/src/lib/logger.d.ts +62 -0
  138. package/dist/src/lib/logger.js +364 -0
  139. package/dist/src/lib/pdf.d.ts +11 -0
  140. package/dist/src/lib/pdf.js +11 -0
  141. package/dist/src/lib/prisma.d.ts +3 -0
  142. package/dist/src/lib/prisma.js +15 -0
  143. package/dist/src/lib/pusher.d.ts +38 -0
  144. package/dist/src/lib/pusher.js +170 -0
  145. package/dist/src/lib/retry.d.ts +15 -0
  146. package/dist/src/lib/retry.js +37 -0
  147. package/dist/src/lib/storage.d.ts +11 -0
  148. package/dist/src/lib/storage.js +71 -0
  149. package/dist/src/lib/stripe.d.ts +10 -0
  150. package/dist/src/lib/stripe.js +36 -0
  151. package/dist/src/lib/validation.d.ts +51 -0
  152. package/dist/src/lib/validation.js +64 -0
  153. package/dist/src/lib/workspace-kb.d.ts +5 -0
  154. package/dist/src/lib/workspace-kb.js +7 -0
  155. package/dist/src/repositories/artifact.repository.d.ts +64 -0
  156. package/dist/src/repositories/artifact.repository.js +40 -0
  157. package/dist/src/repositories/base.repository.d.ts +14 -0
  158. package/dist/src/repositories/base.repository.js +14 -0
  159. package/dist/src/repositories/invitation.repository.d.ts +104 -0
  160. package/dist/src/repositories/invitation.repository.js +44 -0
  161. package/dist/src/repositories/notification.repository.d.ts +76 -0
  162. package/dist/src/repositories/notification.repository.js +44 -0
  163. package/dist/src/repositories/user.repository.d.ts +84 -0
  164. package/dist/src/repositories/user.repository.js +37 -0
  165. package/dist/src/repositories/workspace-member.repository.d.ts +35 -0
  166. package/dist/src/repositories/workspace-member.repository.js +31 -0
  167. package/dist/src/repositories/workspace.repository.d.ts +101 -0
  168. package/dist/src/repositories/workspace.repository.js +79 -0
  169. package/dist/src/routers/_app.d.ts +3464 -0
  170. package/dist/src/routers/_app.js +36 -0
  171. package/dist/src/routers/admin.d.ts +358 -0
  172. package/dist/src/routers/admin.js +105 -0
  173. package/dist/src/routers/annotations.d.ts +219 -0
  174. package/dist/src/routers/annotations.js +29 -0
  175. package/dist/src/routers/artifactVersions.d.ts +65 -0
  176. package/dist/src/routers/artifactVersions.js +14 -0
  177. package/dist/src/routers/auth.d.ts +161 -0
  178. package/dist/src/routers/auth.js +97 -0
  179. package/dist/src/routers/chat.d.ts +170 -0
  180. package/dist/src/routers/chat.js +32 -0
  181. package/dist/src/routers/copilot.d.ts +200 -0
  182. package/dist/src/routers/copilot.js +52 -0
  183. package/dist/src/routers/flashcards.d.ts +336 -0
  184. package/dist/src/routers/flashcards.js +93 -0
  185. package/dist/src/routers/knowledgeBase.d.ts +421 -0
  186. package/dist/src/routers/knowledgeBase.js +118 -0
  187. package/dist/src/routers/members.d.ts +169 -0
  188. package/dist/src/routers/members.js +47 -0
  189. package/dist/src/routers/notifications.d.ts +99 -0
  190. package/dist/src/routers/notifications.js +25 -0
  191. package/dist/src/routers/payment.d.ts +80 -0
  192. package/dist/src/routers/payment.js +21 -0
  193. package/dist/src/routers/podcast.d.ts +287 -0
  194. package/dist/src/routers/podcast.js +34 -0
  195. package/dist/src/routers/studyguide.d.ts +36 -0
  196. package/dist/src/routers/studyguide.js +8 -0
  197. package/dist/src/routers/worksheets.d.ts +429 -0
  198. package/dist/src/routers/worksheets.js +139 -0
  199. package/dist/src/routers/workspace.d.ts +563 -0
  200. package/dist/src/routers/workspace.js +104 -0
  201. package/dist/src/scripts/purge-deleted-users.d.ts +1 -0
  202. package/dist/src/scripts/purge-deleted-users.js +148 -0
  203. package/dist/src/server.d.ts +1 -0
  204. package/dist/src/server.js +190 -0
  205. package/dist/src/services/activity/activity-human-description.service.d.ts +13 -0
  206. package/dist/src/services/activity/activity-human-description.service.js +221 -0
  207. package/dist/src/services/activity/activity-human-description.service.test.d.ts +1 -0
  208. package/dist/src/services/activity/activity-human-description.service.test.js +16 -0
  209. package/dist/src/services/activity/activity-log.service.d.ts +87 -0
  210. package/dist/src/services/activity/activity-log.service.js +276 -0
  211. package/dist/src/services/activity/activity-log.service.test.d.ts +1 -0
  212. package/dist/src/services/activity/activity-log.service.test.js +27 -0
  213. package/dist/src/services/admin/admin.service.d.ts +270 -0
  214. package/dist/src/services/admin/admin.service.js +476 -0
  215. package/dist/src/services/ai/ai-session.service.d.ts +5 -0
  216. package/dist/src/services/ai/ai-session.service.js +4 -0
  217. package/dist/src/services/artifacts/annotation.service.d.ts +177 -0
  218. package/dist/src/services/artifacts/annotation.service.js +154 -0
  219. package/dist/src/services/artifacts/artifact-version.service.d.ts +38 -0
  220. package/dist/src/services/artifacts/artifact-version.service.js +129 -0
  221. package/dist/src/services/artifacts/chat.service.d.ts +127 -0
  222. package/dist/src/services/artifacts/chat.service.js +182 -0
  223. package/dist/src/services/artifacts/study-guide.service.d.ts +18 -0
  224. package/dist/src/services/artifacts/study-guide.service.js +65 -0
  225. package/dist/src/services/auth/auth.service.d.ts +94 -0
  226. package/dist/src/services/auth/auth.service.js +368 -0
  227. package/dist/src/services/base.service.d.ts +14 -0
  228. package/dist/src/services/base.service.js +14 -0
  229. package/dist/src/services/billing/payment.service.d.ts +44 -0
  230. package/dist/src/services/billing/payment.service.js +365 -0
  231. package/dist/src/services/billing/subscription.service.d.ts +37 -0
  232. package/dist/src/services/billing/subscription.service.js +654 -0
  233. package/dist/src/services/billing/usage.service.d.ts +47 -0
  234. package/dist/src/services/billing/usage.service.js +149 -0
  235. package/dist/src/services/content/copilot.service.d.ts +113 -0
  236. package/dist/src/services/content/copilot.service.js +439 -0
  237. package/dist/src/services/content/flashcard-progress.service.d.ts +159 -0
  238. package/dist/src/services/content/flashcard-progress.service.js +432 -0
  239. package/dist/src/services/content/flashcard.service.d.ts +184 -0
  240. package/dist/src/services/content/flashcard.service.js +339 -0
  241. package/dist/src/services/content/media-analysis.service.d.ts +23 -0
  242. package/dist/src/services/content/media-analysis.service.js +404 -0
  243. package/dist/src/services/content/podcast.service.d.ts +267 -0
  244. package/dist/src/services/content/podcast.service.js +653 -0
  245. package/dist/src/services/content/worksheet-content.service.d.ts +37 -0
  246. package/dist/src/services/content/worksheet-content.service.js +84 -0
  247. package/dist/src/services/content/worksheet-content.service.test.d.ts +1 -0
  248. package/dist/src/services/content/worksheet-content.service.test.js +69 -0
  249. package/dist/src/services/content/worksheet-generation.service.d.ts +91 -0
  250. package/dist/src/services/content/worksheet-generation.service.js +95 -0
  251. package/dist/src/services/content/worksheet-generation.service.test.d.ts +1 -0
  252. package/dist/src/services/content/worksheet-generation.service.test.js +20 -0
  253. package/dist/src/services/content/worksheet.service.d.ts +347 -0
  254. package/dist/src/services/content/worksheet.service.js +599 -0
  255. package/dist/src/services/knowledge/knowledge-base.service.d.ts +316 -0
  256. package/dist/src/services/knowledge/knowledge-base.service.js +544 -0
  257. package/dist/src/services/members/invitation.service.d.ts +66 -0
  258. package/dist/src/services/members/invitation.service.js +348 -0
  259. package/dist/src/services/members/member.service.d.ts +36 -0
  260. package/dist/src/services/members/member.service.js +193 -0
  261. package/dist/src/services/notifications/notification.service.d.ts +214 -0
  262. package/dist/src/services/notifications/notification.service.js +550 -0
  263. package/dist/src/services/notifications/notification.service.test.d.ts +1 -0
  264. package/dist/src/services/notifications/notification.service.test.js +87 -0
  265. package/dist/src/services/workspace/workspace-analytics.service.d.ts +24 -0
  266. package/dist/src/services/workspace/workspace-analytics.service.js +95 -0
  267. package/dist/src/services/workspace/workspace-kb.service.d.ts +40 -0
  268. package/dist/src/services/workspace/workspace-kb.service.js +184 -0
  269. package/dist/src/services/workspace/workspace.service.d.ts +263 -0
  270. package/dist/src/services/workspace/workspace.service.js +401 -0
  271. package/dist/src/trpc.d.ts +60 -0
  272. package/dist/src/trpc.js +217 -0
  273. package/dist/src/types/index.d.ts +126 -0
  274. package/dist/src/types/index.js +1 -0
  275. package/package.json +8 -9
  276. package/prisma/schema.prisma +3 -4
  277. package/prisma/seed.mjs +5 -2
  278. package/prisma.config.ts +16 -0
  279. package/src/lib/prisma.ts +18 -9
  280. package/src/scripts/purge-deleted-users.ts +1 -3
  281. package/tsconfig.json +3 -0
@@ -0,0 +1,365 @@
1
+ import { TRPCError } from '@trpc/server';
2
+ import { BaseService } from '../base.service.js';
3
+ import { stripe } from '../../lib/stripe.js';
4
+ import { env } from '../../lib/env.js';
5
+ import { getAccountSummary } from './usage.service.js';
6
+ import { notifyPaymentSucceeded, notifySubscriptionActivated, notifySubscriptionPaymentSucceeded, } from '../notifications/notification.service.js';
7
+ import { upsertSubscriptionFromStripe } from './subscription.service.js';
8
+ /** Stripe Checkout URLs contain the session id (cs_…); we only store the URL in DB. */
9
+ const CHECKOUT_SESSION_ID_IN_URL = /cs_[a-zA-Z0-9]+/;
10
+ async function reuseCheckoutUrlIfSessionStillOpen(stripeClient, db, existing, lockKey) {
11
+ const url = existing.stripeSessionId;
12
+ if (!url)
13
+ return null;
14
+ const idMatch = url.match(CHECKOUT_SESSION_ID_IN_URL);
15
+ if (!idMatch) {
16
+ await db.idempotencyRecord.updateMany({
17
+ where: { id: existing.id, activeLockKey: lockKey },
18
+ data: { activeLockKey: null, status: 'expired' },
19
+ });
20
+ return null;
21
+ }
22
+ try {
23
+ const session = await stripeClient.checkout.sessions.retrieve(idMatch[0]);
24
+ if (session.status === 'open') {
25
+ return url;
26
+ }
27
+ await db.idempotencyRecord.updateMany({
28
+ where: { id: existing.id, activeLockKey: lockKey },
29
+ data: {
30
+ activeLockKey: null,
31
+ status: session.status === 'complete' ? 'completed' : 'expired',
32
+ },
33
+ });
34
+ return null;
35
+ }
36
+ catch {
37
+ await db.idempotencyRecord.updateMany({
38
+ where: { id: existing.id, activeLockKey: lockKey },
39
+ data: { activeLockKey: null, status: 'expired' },
40
+ });
41
+ return null;
42
+ }
43
+ }
44
+ export class PaymentService extends BaseService {
45
+ constructor(db) {
46
+ super(db);
47
+ }
48
+ async getPlans(userId) {
49
+ const plans = await this.db.plan.findMany({
50
+ where: { active: true },
51
+ include: { limit: true },
52
+ orderBy: { price: 'asc' },
53
+ });
54
+ // Anonymous callers (pricing page) don't get per-user `isActive`.
55
+ if (!userId) {
56
+ return plans.map((plan) => ({ ...plan, isActive: false }));
57
+ }
58
+ const activeSubscriptions = await this.db.subscription.findMany({
59
+ where: { userId, status: 'active' },
60
+ });
61
+ return plans.map((plan) => ({
62
+ ...plan,
63
+ isActive: activeSubscriptions.some((sub) => sub.planId === plan.id),
64
+ }));
65
+ }
66
+ async createCheckoutSession(userId, input) {
67
+ if (!stripe) {
68
+ throw new TRPCError({
69
+ code: 'INTERNAL_SERVER_ERROR',
70
+ message: 'Stripe not configured',
71
+ });
72
+ }
73
+ const user = await this.db.user.findUnique({ where: { id: userId } });
74
+ if (!user)
75
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });
76
+ const lockKey = `pending_${userId}_${input.planId}`;
77
+ let attempt = null;
78
+ let retryCount = 0;
79
+ while (retryCount < 3) {
80
+ try {
81
+ attempt = await this.db.idempotencyRecord.create({
82
+ data: {
83
+ userId,
84
+ planId: input.planId,
85
+ activeLockKey: lockKey,
86
+ status: 'pending',
87
+ },
88
+ });
89
+ break;
90
+ }
91
+ catch (err) {
92
+ if (err.code === 'P2002') {
93
+ const existing = await this.db.idempotencyRecord.findUnique({
94
+ where: { activeLockKey: lockKey },
95
+ });
96
+ if (!existing) {
97
+ retryCount++;
98
+ continue;
99
+ }
100
+ const isStale = Date.now() - existing.updatedAt.getTime() > 24 * 60 * 60 * 1000;
101
+ if (isStale) {
102
+ const result = await this.db.idempotencyRecord.updateMany({
103
+ where: { id: existing.id, activeLockKey: lockKey },
104
+ data: { activeLockKey: null, status: 'expired' },
105
+ });
106
+ if (result.count > 0) {
107
+ retryCount++;
108
+ continue;
109
+ }
110
+ }
111
+ if (existing.stripeSessionId && stripe) {
112
+ const reusable = await reuseCheckoutUrlIfSessionStillOpen(stripe, this.db, existing, lockKey);
113
+ if (reusable)
114
+ return { url: reusable };
115
+ retryCount++;
116
+ continue;
117
+ }
118
+ await new Promise((resolve) => setTimeout(resolve, 800));
119
+ retryCount++;
120
+ continue;
121
+ }
122
+ throw err;
123
+ }
124
+ }
125
+ if (!attempt) {
126
+ throw new TRPCError({ code: 'CONFLICT', message: 'Concurrent request' });
127
+ }
128
+ const plan = await this.db.plan.findUnique({ where: { id: input.planId } });
129
+ if (!plan)
130
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'Plan not found' });
131
+ try {
132
+ const successUrl = env.STRIPE_SUCCESS_URL.includes('session_id=')
133
+ ? env.STRIPE_SUCCESS_URL
134
+ : `${env.STRIPE_SUCCESS_URL}${env.STRIPE_SUCCESS_URL.includes('?') ? '&' : '?'}session_id={CHECKOUT_SESSION_ID}`;
135
+ const session = await stripe.checkout.sessions.create({
136
+ customer: user.stripe_customer_id || undefined,
137
+ customer_email: user.stripe_customer_id ? undefined : user.email || undefined,
138
+ line_items: [{ price: plan.stripePriceId, quantity: 1 }],
139
+ mode: plan.interval ? 'subscription' : 'payment',
140
+ subscription_data: plan.interval
141
+ ? {
142
+ metadata: {
143
+ userId,
144
+ planId: plan.id,
145
+ attemptId: attempt.id,
146
+ },
147
+ }
148
+ : undefined,
149
+ success_url: successUrl,
150
+ cancel_url: env.STRIPE_CANCEL_URL,
151
+ metadata: {
152
+ userId,
153
+ planId: plan.id,
154
+ attemptId: attempt.id,
155
+ },
156
+ }, {
157
+ idempotencyKey: attempt.id,
158
+ });
159
+ await this.db.idempotencyRecord.update({
160
+ where: { id: attempt.id },
161
+ data: { stripeSessionId: session.url },
162
+ });
163
+ return { url: session.url };
164
+ }
165
+ catch (error) {
166
+ await this.db.idempotencyRecord.update({
167
+ where: { id: attempt.id },
168
+ data: { status: 'failed', activeLockKey: null },
169
+ });
170
+ throw new TRPCError({
171
+ code: 'INTERNAL_SERVER_ERROR',
172
+ message: error.message,
173
+ });
174
+ }
175
+ }
176
+ async confirmCheckoutSuccess(userId, input) {
177
+ if (!stripe) {
178
+ throw new TRPCError({
179
+ code: 'INTERNAL_SERVER_ERROR',
180
+ message: 'Stripe not configured',
181
+ });
182
+ }
183
+ const session = await stripe.checkout.sessions.retrieve(input.sessionId, {
184
+ expand: ['line_items', 'subscription'],
185
+ });
186
+ const metadata = session.metadata || {};
187
+ if (metadata.userId !== userId) {
188
+ throw new TRPCError({
189
+ code: 'FORBIDDEN',
190
+ message: 'This checkout session does not belong to the current user',
191
+ });
192
+ }
193
+ if (session.status !== 'complete') {
194
+ return { confirmed: false, reason: 'checkout_not_complete' };
195
+ }
196
+ const plan = metadata.planId
197
+ ? await this.db.plan.findUnique({ where: { id: metadata.planId } })
198
+ : null;
199
+ if (session.mode === 'payment' && session.payment_status === 'paid') {
200
+ await notifyPaymentSucceeded(this.db, {
201
+ userId,
202
+ planId: metadata.planId,
203
+ planName: plan?.name || metadata.planType,
204
+ stripeSessionId: session.id,
205
+ amountPaid: session.amount_total ?? undefined,
206
+ });
207
+ return { confirmed: true, kind: 'payment' };
208
+ }
209
+ if (session.mode === 'subscription') {
210
+ const stripeSubscriptionId = typeof session.subscription === 'string'
211
+ ? session.subscription
212
+ : session.subscription?.id;
213
+ if (stripeSubscriptionId) {
214
+ await upsertSubscriptionFromStripe(stripeSubscriptionId);
215
+ await notifySubscriptionActivated(this.db, {
216
+ userId,
217
+ planId: metadata.planId,
218
+ planName: plan?.name || metadata.planType,
219
+ stripeSubscriptionId,
220
+ });
221
+ if (session.payment_status === 'paid') {
222
+ await notifySubscriptionPaymentSucceeded(this.db, {
223
+ userId,
224
+ planId: metadata.planId,
225
+ planName: plan?.name || metadata.planType,
226
+ stripeInvoiceId: `checkout_${session.id}`,
227
+ amountPaid: session.amount_total ?? undefined,
228
+ });
229
+ }
230
+ }
231
+ return { confirmed: true, kind: 'subscription' };
232
+ }
233
+ return { confirmed: false, reason: 'unsupported_mode' };
234
+ }
235
+ async createResourcePurchaseSession(userId, input) {
236
+ if (!stripe) {
237
+ throw new TRPCError({
238
+ code: 'INTERNAL_SERVER_ERROR',
239
+ message: 'Stripe is not configured on the server',
240
+ });
241
+ }
242
+ const user = await this.db.user.findUnique({ where: { id: userId } });
243
+ if (!user) {
244
+ throw new TRPCError({ code: 'NOT_FOUND', message: 'User not found' });
245
+ }
246
+ const resourcePrice = await this.db.resourcePrice.findUnique({
247
+ where: { resourceType: input.resourceType },
248
+ });
249
+ if (!resourcePrice) {
250
+ throw new TRPCError({
251
+ code: 'PRECONDITION_FAILED',
252
+ message: 'Price not set',
253
+ });
254
+ }
255
+ const lockKey = `topup_${userId}_${input.resourceType}`;
256
+ let attempt = null;
257
+ let retryCount = 0;
258
+ while (retryCount < 3) {
259
+ try {
260
+ attempt = await this.db.idempotencyRecord.create({
261
+ data: {
262
+ userId,
263
+ resourceType: input.resourceType,
264
+ activeLockKey: lockKey,
265
+ status: 'pending',
266
+ },
267
+ });
268
+ break;
269
+ }
270
+ catch (err) {
271
+ if (err.code === 'P2002') {
272
+ const existing = await this.db.idempotencyRecord.findUnique({
273
+ where: { activeLockKey: lockKey },
274
+ });
275
+ if (!existing) {
276
+ retryCount++;
277
+ continue;
278
+ }
279
+ const isStale = Date.now() - existing.updatedAt.getTime() > 24 * 60 * 60 * 1000;
280
+ if (isStale) {
281
+ const result = await this.db.idempotencyRecord.updateMany({
282
+ where: { id: existing.id, activeLockKey: lockKey },
283
+ data: { activeLockKey: null, status: 'expired' },
284
+ });
285
+ if (result.count > 0) {
286
+ retryCount++;
287
+ continue;
288
+ }
289
+ }
290
+ if (existing.stripeSessionId && stripe) {
291
+ const reusable = await reuseCheckoutUrlIfSessionStillOpen(stripe, this.db, existing, lockKey);
292
+ if (reusable)
293
+ return { url: reusable };
294
+ retryCount++;
295
+ continue;
296
+ }
297
+ await new Promise((resolve) => setTimeout(resolve, 800));
298
+ retryCount++;
299
+ continue;
300
+ }
301
+ throw err;
302
+ }
303
+ }
304
+ if (!attempt) {
305
+ throw new TRPCError({ code: 'CONFLICT', message: 'Concurrent request' });
306
+ }
307
+ try {
308
+ const resourceName = input.resourceType
309
+ .split('_')
310
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
311
+ .join(' ');
312
+ const session = await stripe.checkout.sessions.create({
313
+ customer: user.stripe_customer_id || undefined,
314
+ line_items: [
315
+ {
316
+ price_data: {
317
+ currency: 'usd',
318
+ product_data: {
319
+ name: `Add-on: extra ${resourceName}s`,
320
+ description: `Purchase of ${input.quantity} additional ${resourceName}(s)`,
321
+ },
322
+ unit_amount: resourcePrice.priceCents,
323
+ },
324
+ quantity: input.quantity,
325
+ },
326
+ ],
327
+ mode: 'payment',
328
+ success_url: `${env.STRIPE_SUCCESS_URL}?success=true`,
329
+ cancel_url: env.STRIPE_CANCEL_URL,
330
+ metadata: {
331
+ userId,
332
+ resourceType: input.resourceType,
333
+ quantity: input.quantity.toString(),
334
+ isPurchase: 'true',
335
+ attemptId: attempt.id,
336
+ },
337
+ invoice_creation: { enabled: true },
338
+ }, {
339
+ idempotencyKey: attempt.id,
340
+ });
341
+ await this.db.idempotencyRecord.update({
342
+ where: { id: attempt.id },
343
+ data: { stripeSessionId: session.url },
344
+ });
345
+ return { url: session.url };
346
+ }
347
+ catch (error) {
348
+ await this.db.idempotencyRecord.update({
349
+ where: { id: attempt.id },
350
+ data: { status: 'failed', activeLockKey: null },
351
+ });
352
+ throw new TRPCError({
353
+ code: 'INTERNAL_SERVER_ERROR',
354
+ message: error.message,
355
+ });
356
+ }
357
+ }
358
+ async getUsageOverview(userId) {
359
+ const { usage, limits, hasActivePlan } = await getAccountSummary(userId);
360
+ return { usage, limits, hasActivePlan };
361
+ }
362
+ getResourcePrices() {
363
+ return this.db.resourcePrice.findMany();
364
+ }
365
+ }
@@ -0,0 +1,37 @@
1
+ import Stripe from 'stripe';
2
+ /**
3
+ * Handle checkout.session.completed event
4
+ */
5
+ export declare function handleCheckoutCompleted(event: Stripe.Event): Promise<void>;
6
+ /**
7
+ * Handle customer.subscription.created event
8
+ */
9
+ export declare function handleSubscriptionCreated(event: Stripe.Event): Promise<void>;
10
+ /**
11
+ * Handle customer.subscription.updated event
12
+ */
13
+ export declare function handleSubscriptionUpdated(event: Stripe.Event): Promise<void>;
14
+ /**
15
+ * Handle customer.subscription.deleted event
16
+ */
17
+ export declare function handleSubscriptionDeleted(event: Stripe.Event): Promise<void>;
18
+ /**
19
+ * Handle invoice.paid event
20
+ */
21
+ export declare function handleInvoicePaid(event: Stripe.Event): Promise<void>;
22
+ /**
23
+ * Handle invoice.payment_failed event
24
+ */
25
+ export declare function handlePaymentFailed(event: Stripe.Event): Promise<void>;
26
+ /**
27
+ * Handle payment_intent.payment_failed event (One-time payments)
28
+ */
29
+ export declare function handlePaymentIntentFailed(event: Stripe.Event): Promise<void>;
30
+ /**
31
+ * Get the storage limit for a user based on their active subscription
32
+ */
33
+ export declare function getUserStorageLimit(userId: string): Promise<number>;
34
+ /**
35
+ * Core logic to sync Stripe subscription state with Prisma database
36
+ */
37
+ export declare function upsertSubscriptionFromStripe(subscriptionIdOrObject: Stripe.Subscription | string): Promise<void>;