@equilateral_ai/mindmeld 3.5.2 → 4.0.1

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 (140) hide show
  1. package/hooks/session-end.js +25 -0
  2. package/hooks/session-start.js +363 -83
  3. package/hooks/session-watcher.js +585 -0
  4. package/package.json +19 -13
  5. package/scripts/init-project.js +9 -23
  6. package/src/client/dbShim.js +16 -0
  7. package/src/core/AuthManager.js +3 -2
  8. package/src/handlers/helpers/dbOperations.js +9 -46
  9. package/src/index.js +2 -217
  10. package/src/utils/piiMask.js +16 -0
  11. package/scripts/harvest.js +0 -601
  12. package/scripts/inject.js +0 -409
  13. package/scripts/mcp-bridge.js +0 -220
  14. package/scripts/repo-analyzer.js +0 -870
  15. package/src/collaboration/CollaborationPrompt.js +0 -460
  16. package/src/core/AlertEngine.js +0 -813
  17. package/src/core/AlertNotifier.js +0 -363
  18. package/src/core/CorrelationAnalyzer.js +0 -931
  19. package/src/core/CrossReferenceEngine.js +0 -624
  20. package/src/core/CurationEngine.js +0 -688
  21. package/src/core/DeprecationScheduler.js +0 -183
  22. package/src/core/LoadBearingDetector.js +0 -242
  23. package/src/core/NotificationService.js +0 -1032
  24. package/src/core/RapportOrchestrator.js +0 -632
  25. package/src/core/RelevanceDetector.js +0 -694
  26. package/src/core/StandardLifecycle.js +0 -244
  27. package/src/core/StandardsIngestion.js +0 -991
  28. package/src/core/TeamLoadBearingDetector.js +0 -431
  29. package/src/core/parsers/adrParser.js +0 -479
  30. package/src/core/parsers/cursorRulesParser.js +0 -564
  31. package/src/core/parsers/eslintParser.js +0 -439
  32. package/src/database/dbOperations.js +0 -105
  33. package/src/handlers/activity/activityGetMe.js +0 -98
  34. package/src/handlers/activity/activityGetTeam.js +0 -175
  35. package/src/handlers/admin/adminSetup.js +0 -216
  36. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  37. package/src/handlers/alerts/alertsGet.js +0 -250
  38. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  39. package/src/handlers/analytics/coachingGet.js +0 -361
  40. package/src/handlers/analytics/convergenceGet.js +0 -236
  41. package/src/handlers/analytics/developerScoreGet.js +0 -137
  42. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  43. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  44. package/src/handlers/collaborators/collaboratorList.js +0 -82
  45. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  46. package/src/handlers/collaborators/inviteAccept.js +0 -122
  47. package/src/handlers/company/companyUsersDelete.js +0 -141
  48. package/src/handlers/company/companyUsersGet.js +0 -90
  49. package/src/handlers/company/companyUsersPost.js +0 -267
  50. package/src/handlers/company/companyUsersPut.js +0 -76
  51. package/src/handlers/context/contextGet.js +0 -57
  52. package/src/handlers/context/invariantsGet.js +0 -74
  53. package/src/handlers/context/loopsGet.js +0 -82
  54. package/src/handlers/context/notesCreate.js +0 -74
  55. package/src/handlers/context/purposeGet.js +0 -78
  56. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  57. package/src/handlers/correlations/correlationsGet.js +0 -93
  58. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  59. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  60. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  61. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  62. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  63. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  64. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  65. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  66. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  67. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  68. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  69. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  70. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  71. package/src/handlers/github/githubConnectionStatus.js +0 -49
  72. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  73. package/src/handlers/github/githubOAuthCallback.js +0 -178
  74. package/src/handlers/github/githubOAuthStart.js +0 -59
  75. package/src/handlers/github/githubPatternsReview.js +0 -76
  76. package/src/handlers/github/githubReposList.js +0 -105
  77. package/src/handlers/health/healthGet.js +0 -55
  78. package/src/handlers/helpers/auditLogger.js +0 -201
  79. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  80. package/src/handlers/helpers/decisionFrames.js +0 -29
  81. package/src/handlers/helpers/errorHandler.js +0 -49
  82. package/src/handlers/helpers/index.js +0 -138
  83. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  84. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  85. package/src/handlers/helpers/predictiveCache.js +0 -51
  86. package/src/handlers/helpers/projectAccess.js +0 -88
  87. package/src/handlers/helpers/responseUtil.js +0 -55
  88. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  89. package/src/handlers/mcp/mcpHandler.js +0 -569
  90. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  91. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  92. package/src/handlers/notifications/getPreferences.js +0 -84
  93. package/src/handlers/notifications/sendNotification.js +0 -170
  94. package/src/handlers/notifications/updatePreferences.js +0 -316
  95. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  96. package/src/handlers/patterns/patternUsagePost.js +0 -182
  97. package/src/handlers/patterns/patternViolationPost.js +0 -185
  98. package/src/handlers/projects/projectCreate.js +0 -248
  99. package/src/handlers/projects/projectDelete.js +0 -82
  100. package/src/handlers/projects/projectGet.js +0 -95
  101. package/src/handlers/projects/projectUpdate.js +0 -117
  102. package/src/handlers/reports/aiLeverage.js +0 -210
  103. package/src/handlers/reports/engineeringInvestment.js +0 -132
  104. package/src/handlers/reports/riskForecast.js +0 -206
  105. package/src/handlers/reports/standardsRoi.js +0 -254
  106. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  107. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  108. package/src/handlers/scheduled/generateAlerts.js +0 -135
  109. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  110. package/src/handlers/scheduled/refreshActivity.js +0 -21
  111. package/src/handlers/scheduled/scanCompliance.js +0 -334
  112. package/src/handlers/sessions/sessionEndPost.js +0 -180
  113. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  114. package/src/handlers/standards/catalogGet.js +0 -185
  115. package/src/handlers/standards/catalogSync.js +0 -120
  116. package/src/handlers/standards/discoveriesGet.js +0 -89
  117. package/src/handlers/standards/projectStandardsGet.js +0 -129
  118. package/src/handlers/standards/projectStandardsPut.js +0 -151
  119. package/src/handlers/standards/standardsAuditGet.js +0 -65
  120. package/src/handlers/standards/standardsParseUpload.js +0 -149
  121. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  122. package/src/handlers/standards/standardsTransition.js +0 -161
  123. package/src/handlers/stripe/addonManagePost.js +0 -240
  124. package/src/handlers/stripe/billingPortalPost.js +0 -93
  125. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  126. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  127. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  128. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  129. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  130. package/src/handlers/stripe/webhookPost.js +0 -482
  131. package/src/handlers/user/apiTokenCreate.js +0 -71
  132. package/src/handlers/user/apiTokenList.js +0 -64
  133. package/src/handlers/user/userSplashAck.js +0 -91
  134. package/src/handlers/user/userSplashGet.js +0 -211
  135. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  136. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  137. package/src/handlers/users/userEntitlementsGet.js +0 -89
  138. package/src/handlers/users/userGet.js +0 -118
  139. package/src/handlers/users/userProfilePut.js +0 -77
  140. package/src/handlers/webhooks/githubWebhook.js +0 -215
@@ -1,163 +0,0 @@
1
- /**
2
- * Subscription Update Handler
3
- * Upgrades or downgrades subscription tier
4
- *
5
- * PUT /api/stripe/subscription/update
6
- * Body: { newTier, billingPeriod }
7
- * Auth: Cognito JWT required
8
- */
9
-
10
- const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse, handleError, getTierConfig, getStripePriceId } = require('./helpers');
11
-
12
- // Stripe is optional
13
- let stripe = null;
14
- try {
15
- if (process.env.STRIPE_SECRET_KEY) {
16
- stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
17
- }
18
- } catch (e) {
19
- console.warn('Stripe not available:', e.message);
20
- }
21
-
22
- /**
23
- * Update subscription tier
24
- */
25
- async function updateSubscription({ body: requestBody = {}, requestContext }) {
26
- try {
27
- const Request_ID = requestContext.requestId;
28
- // REST API: requestContext.authorizer.claims.email
29
- // HTTP API: requestContext.authorizer.jwt.claims.email
30
- const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
31
-
32
- if (!email) {
33
- return createErrorResponse(401, 'Authentication required');
34
- }
35
-
36
- const { newTier, billingPeriod = 'monthly' } = requestBody;
37
-
38
- // Validate new tier
39
- const newTierConfig = getTierConfig(newTier);
40
- if (!newTierConfig || newTier === 'free') {
41
- return createErrorResponse(400, 'Invalid tier. Use cancel endpoint for downgrade to free.', {
42
- valid_tiers: ['team', 'professional', 'enterprise']
43
- });
44
- }
45
-
46
- if (!stripe) {
47
- return createErrorResponse(503, 'Payment system not configured');
48
- }
49
-
50
- // Get user's current subscription
51
- const userQuery = `
52
- SELECT
53
- u.client_id,
54
- c.stripe_customer_id,
55
- c.subscription_tier,
56
- c.subscription_status
57
- FROM rapport.users u
58
- JOIN rapport.clients c ON u.client_id = c.client_id
59
- WHERE u.email_address = $1 AND u.active = true
60
- `;
61
- const userResult = await executeQuery(userQuery, [email]);
62
-
63
- if (userResult.rowCount === 0) {
64
- return createErrorResponse(404, 'User not found');
65
- }
66
-
67
- const user = userResult.rows[0];
68
- const currentTierConfig = getTierConfig(user.subscription_tier);
69
-
70
- if (!user.stripe_customer_id || user.subscription_status !== 'active') {
71
- return createErrorResponse(400, 'No active subscription. Use create endpoint first.');
72
- }
73
-
74
- // Get active subscription from Stripe
75
- const subscriptions = await stripe.subscriptions.list({
76
- customer: user.stripe_customer_id,
77
- status: 'active',
78
- limit: 1
79
- });
80
-
81
- if (subscriptions.data.length === 0) {
82
- return createErrorResponse(400, 'No active subscription found in Stripe');
83
- }
84
-
85
- const subscription = subscriptions.data[0];
86
- const newPriceId = getStripePriceId(newTier, billingPeriod);
87
-
88
- if (!newPriceId) {
89
- return createErrorResponse(500, 'Price not configured for tier');
90
- }
91
-
92
- // Count users for per-seat pricing
93
- const usageQuery = `
94
- SELECT COUNT(*) as user_count
95
- FROM rapport.user_entitlements
96
- WHERE client_id = $1
97
- `;
98
- const usageResult = await executeQuery(usageQuery, [user.client_id]);
99
- const userCount = parseInt(usageResult.rows[0].user_count) || 1;
100
-
101
- // Determine if upgrade or downgrade
102
- const isUpgrade = (newTierConfig.priceMonthly || 0) > (currentTierConfig?.priceMonthly || 0);
103
-
104
- // Update subscription in Stripe
105
- const updatedSubscription = await stripe.subscriptions.update(subscription.id, {
106
- items: [{
107
- id: subscription.items.data[0].id,
108
- price: newPriceId,
109
- quantity: newTierConfig.perUser ? userCount : 1
110
- }],
111
- proration_behavior: 'create_prorations',
112
- metadata: {
113
- client_id: user.client_id,
114
- tier: newTier
115
- }
116
- });
117
-
118
- // Update local database
119
- await executeQuery(`
120
- UPDATE rapport.clients
121
- SET subscription_tier = $2,
122
- last_updated = CURRENT_TIMESTAMP
123
- WHERE client_id = $1
124
- `, [user.client_id, newTier]);
125
-
126
- // Calculate proration info
127
- const currentPeriodEnd = new Date(subscription.current_period_end * 1000);
128
- const daysRemaining = Math.ceil((currentPeriodEnd - new Date()) / (1000 * 60 * 60 * 24));
129
-
130
- return createSuccessResponse(
131
- {
132
- Records: [{
133
- previous_tier: user.subscription_tier,
134
- new_tier: newTier,
135
- new_tier_name: newTierConfig.displayName,
136
- is_upgrade: isUpgrade,
137
- billing_period: billingPeriod,
138
- user_count: newTierConfig.perUser ? userCount : null,
139
- monthly_cost: newTierConfig.perUser
140
- ? newTierConfig.priceMonthly * userCount
141
- : newTierConfig.priceMonthly,
142
- proration: {
143
- applied: true,
144
- days_remaining: daysRemaining,
145
- current_period_end: currentPeriodEnd.toISOString()
146
- }
147
- }]
148
- },
149
- isUpgrade ? 'Subscription upgraded' : 'Subscription changed',
150
- {
151
- Total_Records: 1,
152
- Request_ID,
153
- Timestamp: new Date().toISOString()
154
- }
155
- );
156
-
157
- } catch (error) {
158
- console.error('Handler Error:', error);
159
- return handleError(error);
160
- }
161
- }
162
-
163
- exports.handler = wrapHandler(updateSubscription);
@@ -1,482 +0,0 @@
1
- /**
2
- * Stripe Webhook Handler
3
- * Processes Stripe webhook events for subscription lifecycle
4
- *
5
- * POST /api/stripe/webhook
6
- * Auth: Stripe signature verification (no Cognito)
7
- *
8
- * Following HoneyDo webhookPost.js pattern
9
- */
10
-
11
- const { executeQuery } = require('./helpers');
12
- const crypto = require('crypto');
13
-
14
- // Stripe is optional
15
- let stripe = null;
16
- try {
17
- if (process.env.STRIPE_SECRET_KEY) {
18
- stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
19
- }
20
- } catch (e) {
21
- console.warn('Stripe not available:', e.message);
22
- }
23
-
24
- /**
25
- * Verify Stripe webhook signature
26
- * Uses timing-safe comparison to prevent timing attacks
27
- */
28
- function verifySignature(payload, signature, secret) {
29
- const parts = signature.split(',').reduce((acc, part) => {
30
- const [key, value] = part.split('=');
31
- acc[key] = value;
32
- return acc;
33
- }, {});
34
-
35
- const timestamp = parts.t;
36
- const expectedSig = parts.v1;
37
-
38
- if (!timestamp || !expectedSig) {
39
- return false;
40
- }
41
-
42
- // Check timestamp is within 5 minutes
43
- const now = Math.floor(Date.now() / 1000);
44
- if (Math.abs(now - parseInt(timestamp)) > 300) {
45
- console.warn('Webhook timestamp too old');
46
- return false;
47
- }
48
-
49
- // Compute expected signature
50
- const signedPayload = `${timestamp}.${payload}`;
51
- const computedSig = crypto
52
- .createHmac('sha256', secret)
53
- .update(signedPayload)
54
- .digest('hex');
55
-
56
- // Timing-safe comparison
57
- try {
58
- return crypto.timingSafeEqual(
59
- Buffer.from(expectedSig),
60
- Buffer.from(computedSig)
61
- );
62
- } catch {
63
- return false;
64
- }
65
- }
66
-
67
- /**
68
- * Handle checkout.session.completed
69
- * New subscription created
70
- */
71
- async function handleCheckoutCompleted(session) {
72
- const { client_id, tier, user_email, enterprise_package, seat_count, addons } = session.metadata || {};
73
-
74
- if (!client_id) {
75
- console.error('[webhookPost] CRITICAL: Missing client_id in checkout session metadata — subscription will not activate');
76
- throw new Error('Missing client_id in checkout session metadata');
77
- }
78
-
79
- // Parse enterprise metadata
80
- const isEnterprise = tier === 'enterprise';
81
- const seatCountNum = seat_count ? parseInt(seat_count, 10) : 1;
82
- let addonsList = [];
83
- if (addons) {
84
- try { addonsList = JSON.parse(addons); }
85
- catch { console.warn('Failed to parse addons metadata:', addons); }
86
- }
87
-
88
- // Update client with subscription
89
- if (isEnterprise) {
90
- const updateResult = await executeQuery(`
91
- UPDATE rapport.clients
92
- SET stripe_customer_id = $2,
93
- stripe_subscription_id = $3,
94
- subscription_tier = 'enterprise',
95
- subscription_status = 'active',
96
- enterprise_package = $4,
97
- seat_count = $5,
98
- subscribed_addons = $6,
99
- last_updated = CURRENT_TIMESTAMP
100
- WHERE client_id = $1
101
- `, [client_id, session.customer, session.subscription, enterprise_package, seatCountNum, JSON.stringify(addonsList)]);
102
-
103
- if (updateResult.rowCount === 0) {
104
- console.error(`[webhookPost] CRITICAL: Client ${client_id} not found in DB — subscription paid but not activated`);
105
- throw new Error(`Client ${client_id} not found — subscription activation failed`);
106
- }
107
-
108
- // Create addon entitlements if any
109
- if (addonsList.length > 0) {
110
- for (const addonId of addonsList) {
111
- await executeQuery(`
112
- INSERT INTO rapport.addon_entitlements (client_id, addon_id, seat_count, status)
113
- VALUES ($1, $2, $3, 'active')
114
- ON CONFLICT (client_id, addon_id) DO UPDATE SET
115
- seat_count = EXCLUDED.seat_count,
116
- status = 'active',
117
- updated_at = CURRENT_TIMESTAMP
118
- `, [client_id, addonId, seatCountNum]);
119
- }
120
- }
121
-
122
- console.log('Enterprise checkout completed:', { client_id, enterprise_package, seat_count: seatCountNum, addons: addonsList });
123
- } else {
124
- const updateResult = await executeQuery(`
125
- UPDATE rapport.clients
126
- SET stripe_customer_id = $2,
127
- stripe_subscription_id = $3,
128
- subscription_tier = $4,
129
- subscription_status = 'active',
130
- last_updated = CURRENT_TIMESTAMP
131
- WHERE client_id = $1
132
- `, [client_id, session.customer, session.subscription, tier || 'team']);
133
-
134
- if (updateResult.rowCount === 0) {
135
- console.error(`[webhookPost] CRITICAL: Client ${client_id} not found in DB — subscription paid but not activated`);
136
- throw new Error(`Client ${client_id} not found — subscription activation failed`);
137
- }
138
-
139
- console.log('Checkout completed:', { client_id, tier, customer: session.customer });
140
- }
141
-
142
- // Update checkout session record
143
- await executeQuery(`
144
- UPDATE rapport.stripe_checkout_sessions
145
- SET status = 'completed',
146
- stripe_customer_id = $2,
147
- stripe_subscription_id = $3,
148
- completed_at = CURRENT_TIMESTAMP
149
- WHERE session_id = $1
150
- `, [session.id, session.customer, session.subscription]);
151
- }
152
-
153
- /**
154
- * Handle customer.subscription.updated
155
- * Subscription tier/status changed, seat count changed, addons changed
156
- */
157
- async function handleSubscriptionUpdated(subscription) {
158
- const { client_id, tier, enterprise_package, seat_count, addons } = subscription.metadata || {};
159
-
160
- // Find client by customer ID if not in metadata
161
- let clientId = client_id;
162
- if (!clientId) {
163
- const result = await executeQuery(`
164
- SELECT client_id FROM rapport.clients WHERE stripe_customer_id = $1
165
- `, [subscription.customer]);
166
-
167
- if (result.rowCount === 0) {
168
- console.warn('Client not found for subscription update');
169
- return;
170
- }
171
- clientId = result.rows[0].client_id;
172
- }
173
-
174
- // Parse enterprise metadata
175
- const isEnterprise = tier === 'enterprise';
176
- const seatCountNum = seat_count ? parseInt(seat_count, 10) : null;
177
- let addonsList = null;
178
- if (addons) {
179
- try { addonsList = JSON.parse(addons); }
180
- catch { console.warn('Failed to parse addons metadata in subscription update:', addons); }
181
- }
182
-
183
- // Build update query based on what changed
184
- if (isEnterprise) {
185
- // Calculate seat count from subscription items quantity
186
- let calculatedSeats = seatCountNum;
187
- if (!calculatedSeats && subscription.items?.data?.length > 0) {
188
- // Each item quantity represents seat packs (1 pack = 25 seats)
189
- const seatPacks = subscription.items.data[0].quantity || 1;
190
- calculatedSeats = seatPacks * 25;
191
- }
192
-
193
- await executeQuery(`
194
- UPDATE rapport.clients
195
- SET subscription_status = $2,
196
- subscription_tier = 'enterprise',
197
- enterprise_package = COALESCE($3, enterprise_package),
198
- seat_count = COALESCE($4, seat_count),
199
- subscribed_addons = COALESCE($5, subscribed_addons),
200
- stripe_subscription_id = $6,
201
- last_updated = CURRENT_TIMESTAMP
202
- WHERE client_id = $1
203
- `, [
204
- clientId,
205
- subscription.status,
206
- enterprise_package,
207
- calculatedSeats,
208
- addonsList ? JSON.stringify(addonsList) : null,
209
- subscription.id
210
- ]);
211
-
212
- console.log('Enterprise subscription updated:', {
213
- client_id: clientId,
214
- status: subscription.status,
215
- seat_count: calculatedSeats
216
- });
217
- } else {
218
- await executeQuery(`
219
- UPDATE rapport.clients
220
- SET subscription_tier = COALESCE($2, subscription_tier),
221
- subscription_status = $3,
222
- stripe_subscription_id = $4,
223
- last_updated = CURRENT_TIMESTAMP
224
- WHERE client_id = $1
225
- `, [clientId, tier, subscription.status, subscription.id]);
226
-
227
- console.log('Subscription updated:', { client_id: clientId, status: subscription.status });
228
- }
229
- }
230
-
231
- /**
232
- * Handle customer.subscription.deleted
233
- * Subscription canceled
234
- */
235
- async function handleSubscriptionDeleted(subscription) {
236
- // Find client by customer ID
237
- const result = await executeQuery(`
238
- SELECT client_id FROM rapport.clients WHERE stripe_customer_id = $1
239
- `, [subscription.customer]);
240
-
241
- if (result.rowCount === 0) {
242
- console.warn('Client not found for subscription deletion');
243
- return;
244
- }
245
-
246
- const clientId = result.rows[0].client_id;
247
-
248
- await executeQuery(`
249
- UPDATE rapport.clients
250
- SET subscription_tier = 'free',
251
- subscription_status = 'canceled',
252
- subscription_ends_at = CURRENT_TIMESTAMP,
253
- last_updated = CURRENT_TIMESTAMP
254
- WHERE client_id = $1
255
- `, [clientId]);
256
-
257
- console.log('Subscription deleted, reverted to free:', { client_id: clientId });
258
- }
259
-
260
- /**
261
- * Handle invoice.payment_succeeded
262
- * Record successful payment
263
- */
264
- async function handlePaymentSucceeded(invoice) {
265
- // Find client by customer
266
- const result = await executeQuery(`
267
- SELECT client_id FROM rapport.clients WHERE stripe_customer_id = $1
268
- `, [invoice.customer]);
269
-
270
- if (result.rowCount === 0) {
271
- console.warn('Client not found for payment');
272
- return;
273
- }
274
-
275
- const clientId = result.rows[0].client_id;
276
-
277
- await executeQuery(`
278
- INSERT INTO rapport.payment_history (
279
- stripe_invoice_id,
280
- stripe_subscription_id,
281
- client_id,
282
- amount_cents,
283
- currency,
284
- status,
285
- paid_at
286
- )
287
- VALUES ($1, $2, $3, $4, $5, 'succeeded', $6)
288
- ON CONFLICT (stripe_invoice_id) DO UPDATE SET
289
- status = 'succeeded',
290
- paid_at = EXCLUDED.paid_at
291
- `, [
292
- invoice.id,
293
- invoice.subscription,
294
- clientId,
295
- invoice.amount_paid,
296
- invoice.currency,
297
- new Date(invoice.status_transitions?.paid_at * 1000 || Date.now())
298
- ]);
299
-
300
- console.log('Payment succeeded:', { invoice: invoice.id, amount: invoice.amount_paid });
301
- }
302
-
303
- /**
304
- * Handle invoice.payment_failed
305
- * Record failed payment
306
- */
307
- async function handlePaymentFailed(invoice) {
308
- const result = await executeQuery(`
309
- SELECT client_id FROM rapport.clients WHERE stripe_customer_id = $1
310
- `, [invoice.customer]);
311
-
312
- if (result.rowCount === 0) {
313
- return;
314
- }
315
-
316
- const clientId = result.rows[0].client_id;
317
-
318
- // Record failed payment
319
- await executeQuery(`
320
- INSERT INTO rapport.payment_history (
321
- stripe_invoice_id,
322
- stripe_subscription_id,
323
- client_id,
324
- amount_cents,
325
- currency,
326
- status,
327
- failure_message
328
- )
329
- VALUES ($1, $2, $3, $4, $5, 'failed', $6)
330
- ON CONFLICT (stripe_invoice_id) DO UPDATE SET
331
- status = 'failed',
332
- failure_message = EXCLUDED.failure_message
333
- `, [
334
- invoice.id,
335
- invoice.subscription,
336
- clientId,
337
- invoice.amount_due,
338
- invoice.currency,
339
- invoice.last_finalization_error?.message || 'Payment failed'
340
- ]);
341
-
342
- // Update subscription status
343
- await executeQuery(`
344
- UPDATE rapport.clients
345
- SET subscription_status = 'past_due',
346
- last_updated = CURRENT_TIMESTAMP
347
- WHERE client_id = $1
348
- `, [clientId]);
349
-
350
- console.log('Payment failed:', { invoice: invoice.id, client_id: clientId });
351
- }
352
-
353
- /**
354
- * Main webhook handler
355
- */
356
- async function handler(event, context) {
357
- try {
358
- const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
359
-
360
- if (!webhookSecret) {
361
- console.error('STRIPE_WEBHOOK_SECRET not configured');
362
- return {
363
- statusCode: 500,
364
- headers: { 'Content-Type': 'application/json' },
365
- body: JSON.stringify({ error: 'Webhook not configured' })
366
- };
367
- }
368
-
369
- const signature = event.headers['stripe-signature'] || event.headers['Stripe-Signature'];
370
- const payload = event.body;
371
-
372
- if (!signature || !payload) {
373
- return {
374
- statusCode: 400,
375
- headers: { 'Content-Type': 'application/json' },
376
- body: JSON.stringify({ error: 'Missing signature or payload' })
377
- };
378
- }
379
-
380
- // Verify signature
381
- if (!verifySignature(payload, signature, webhookSecret)) {
382
- console.warn('Invalid webhook signature');
383
- return {
384
- statusCode: 400,
385
- headers: { 'Content-Type': 'application/json' },
386
- body: JSON.stringify({ error: 'Invalid signature' })
387
- };
388
- }
389
-
390
- const stripeEvent = JSON.parse(payload);
391
-
392
- // Check for duplicate (idempotency)
393
- const dupCheck = await executeQuery(`
394
- SELECT event_id FROM rapport.stripe_webhook_events WHERE event_id = $1
395
- `, [stripeEvent.id]);
396
-
397
- if (dupCheck.rowCount > 0) {
398
- console.log('Duplicate event, skipping:', stripeEvent.id);
399
- return {
400
- statusCode: 200,
401
- headers: { 'Content-Type': 'application/json' },
402
- body: JSON.stringify({ received: true, duplicate: true })
403
- };
404
- }
405
-
406
- // Record event before processing
407
- await executeQuery(`
408
- INSERT INTO rapport.stripe_webhook_events (event_id, event_type, handled)
409
- VALUES ($1, $2, false)
410
- `, [stripeEvent.id, stripeEvent.type]);
411
-
412
- // Process by event type
413
- try {
414
- switch (stripeEvent.type) {
415
- case 'checkout.session.completed':
416
- await handleCheckoutCompleted(stripeEvent.data.object);
417
- break;
418
- case 'customer.subscription.updated':
419
- await handleSubscriptionUpdated(stripeEvent.data.object);
420
- break;
421
- case 'customer.subscription.deleted':
422
- await handleSubscriptionDeleted(stripeEvent.data.object);
423
- break;
424
- case 'invoice.payment_succeeded':
425
- await handlePaymentSucceeded(stripeEvent.data.object);
426
- break;
427
- case 'invoice.payment_failed':
428
- await handlePaymentFailed(stripeEvent.data.object);
429
- break;
430
- default:
431
- console.log('Unhandled event type:', stripeEvent.type);
432
- }
433
-
434
- // Mark as handled
435
- await executeQuery(`
436
- UPDATE rapport.stripe_webhook_events
437
- SET handled = true, processed_at = CURRENT_TIMESTAMP
438
- WHERE event_id = $1
439
- `, [stripeEvent.id]);
440
-
441
- } catch (processError) {
442
- console.error('Error processing webhook:', processError);
443
-
444
- // Record error
445
- try {
446
- await executeQuery(`
447
- UPDATE rapport.stripe_webhook_events
448
- SET handled = false,
449
- error = $2,
450
- processed_at = CURRENT_TIMESTAMP
451
- WHERE event_id = $1
452
- `, [stripeEvent.id, processError.message]);
453
- } catch (recordErr) {
454
- console.error('Failed to record webhook error:', recordErr.message);
455
- }
456
-
457
- // Return 500 for critical failures so Stripe retries
458
- // (missing client, DB connection errors, subscription not activated)
459
- return {
460
- statusCode: 500,
461
- headers: { 'Content-Type': 'application/json' },
462
- body: JSON.stringify({ error: processError.message })
463
- };
464
- }
465
-
466
- return {
467
- statusCode: 200,
468
- headers: { 'Content-Type': 'application/json' },
469
- body: JSON.stringify({ received: true })
470
- };
471
-
472
- } catch (error) {
473
- console.error('Webhook handler error:', error);
474
- return {
475
- statusCode: 500,
476
- headers: { 'Content-Type': 'application/json' },
477
- body: JSON.stringify({ error: 'Internal error' })
478
- };
479
- }
480
- }
481
-
482
- module.exports = { handler };