@equilateral_ai/mindmeld 3.5.3 → 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 (139) hide show
  1. package/hooks/session-start.js +312 -85
  2. package/package.json +20 -14
  3. package/scripts/init-project.js +9 -23
  4. package/src/client/dbShim.js +16 -0
  5. package/src/core/AuthManager.js +3 -2
  6. package/src/handlers/helpers/dbOperations.js +9 -46
  7. package/src/index.js +2 -217
  8. package/src/utils/piiMask.js +16 -0
  9. package/scripts/harvest.js +0 -601
  10. package/scripts/inject.js +0 -409
  11. package/scripts/mcp-bridge.js +0 -220
  12. package/scripts/repo-analyzer.js +0 -870
  13. package/scripts/standards.js +0 -285
  14. package/src/collaboration/CollaborationPrompt.js +0 -460
  15. package/src/core/AlertEngine.js +0 -813
  16. package/src/core/AlertNotifier.js +0 -363
  17. package/src/core/CorrelationAnalyzer.js +0 -931
  18. package/src/core/CrossReferenceEngine.js +0 -624
  19. package/src/core/CurationEngine.js +0 -688
  20. package/src/core/DeprecationScheduler.js +0 -183
  21. package/src/core/LoadBearingDetector.js +0 -242
  22. package/src/core/NotificationService.js +0 -1032
  23. package/src/core/RapportOrchestrator.js +0 -632
  24. package/src/core/RelevanceDetector.js +0 -694
  25. package/src/core/StandardLifecycle.js +0 -244
  26. package/src/core/StandardsIngestion.js +0 -991
  27. package/src/core/TeamLoadBearingDetector.js +0 -431
  28. package/src/core/parsers/adrParser.js +0 -479
  29. package/src/core/parsers/cursorRulesParser.js +0 -564
  30. package/src/core/parsers/eslintParser.js +0 -439
  31. package/src/database/dbOperations.js +0 -105
  32. package/src/handlers/activity/activityGetMe.js +0 -98
  33. package/src/handlers/activity/activityGetTeam.js +0 -175
  34. package/src/handlers/admin/adminSetup.js +0 -216
  35. package/src/handlers/alerts/alertsAcknowledge.js +0 -92
  36. package/src/handlers/alerts/alertsGet.js +0 -250
  37. package/src/handlers/analytics/activitySummaryGet.js +0 -234
  38. package/src/handlers/analytics/coachingGet.js +0 -361
  39. package/src/handlers/analytics/convergenceGet.js +0 -236
  40. package/src/handlers/analytics/developerScoreGet.js +0 -137
  41. package/src/handlers/collaborators/collaboratorAdd.js +0 -200
  42. package/src/handlers/collaborators/collaboratorInvite.js +0 -219
  43. package/src/handlers/collaborators/collaboratorList.js +0 -82
  44. package/src/handlers/collaborators/collaboratorRemove.js +0 -128
  45. package/src/handlers/collaborators/inviteAccept.js +0 -122
  46. package/src/handlers/company/companyUsersDelete.js +0 -141
  47. package/src/handlers/company/companyUsersGet.js +0 -90
  48. package/src/handlers/company/companyUsersPost.js +0 -267
  49. package/src/handlers/company/companyUsersPut.js +0 -76
  50. package/src/handlers/context/contextGet.js +0 -57
  51. package/src/handlers/context/invariantsGet.js +0 -74
  52. package/src/handlers/context/loopsGet.js +0 -82
  53. package/src/handlers/context/notesCreate.js +0 -74
  54. package/src/handlers/context/purposeGet.js +0 -78
  55. package/src/handlers/correlations/correlationsDeveloperGet.js +0 -227
  56. package/src/handlers/correlations/correlationsGet.js +0 -93
  57. package/src/handlers/correlations/correlationsProjectGet.js +0 -153
  58. package/src/handlers/enterprise/controlTowerGet.js +0 -224
  59. package/src/handlers/enterprise/enterpriseAuditGet.js +0 -108
  60. package/src/handlers/enterprise/enterpriseContributorsGet.js +0 -85
  61. package/src/handlers/enterprise/enterpriseKnowledgeCategoriesGet.js +0 -53
  62. package/src/handlers/enterprise/enterpriseKnowledgeCreate.js +0 -77
  63. package/src/handlers/enterprise/enterpriseKnowledgeDelete.js +0 -71
  64. package/src/handlers/enterprise/enterpriseKnowledgeGet.js +0 -87
  65. package/src/handlers/enterprise/enterpriseKnowledgeUpdate.js +0 -122
  66. package/src/handlers/enterprise/enterpriseOnboardingComplete.js +0 -77
  67. package/src/handlers/enterprise/enterpriseOnboardingInvite.js +0 -138
  68. package/src/handlers/enterprise/enterpriseOnboardingSetup.js +0 -128
  69. package/src/handlers/enterprise/enterpriseOnboardingStatus.js +0 -88
  70. package/src/handlers/github/githubConnectionStatus.js +0 -49
  71. package/src/handlers/github/githubDiscoverPatterns.js +0 -621
  72. package/src/handlers/github/githubOAuthCallback.js +0 -178
  73. package/src/handlers/github/githubOAuthStart.js +0 -59
  74. package/src/handlers/github/githubPatternsReview.js +0 -76
  75. package/src/handlers/github/githubReposList.js +0 -105
  76. package/src/handlers/health/healthGet.js +0 -55
  77. package/src/handlers/helpers/auditLogger.js +0 -201
  78. package/src/handlers/helpers/checkSuperAdmin.js +0 -84
  79. package/src/handlers/helpers/decisionFrames.js +0 -29
  80. package/src/handlers/helpers/errorHandler.js +0 -49
  81. package/src/handlers/helpers/index.js +0 -138
  82. package/src/handlers/helpers/lambdaWrapper.js +0 -60
  83. package/src/handlers/helpers/mindmeldMcpCore.js +0 -1103
  84. package/src/handlers/helpers/predictiveCache.js +0 -51
  85. package/src/handlers/helpers/projectAccess.js +0 -88
  86. package/src/handlers/helpers/responseUtil.js +0 -55
  87. package/src/handlers/helpers/subscriptionTiers.js +0 -1168
  88. package/src/handlers/mcp/mcpHandler.js +0 -569
  89. package/src/handlers/mcp/mindmeldMcpHandler.js +0 -124
  90. package/src/handlers/mcp/mindmeldMcpStreamHandler.js +0 -342
  91. package/src/handlers/notifications/getPreferences.js +0 -84
  92. package/src/handlers/notifications/sendNotification.js +0 -170
  93. package/src/handlers/notifications/updatePreferences.js +0 -316
  94. package/src/handlers/patterns/patternEvaluatePromotionPost.js +0 -173
  95. package/src/handlers/patterns/patternUsagePost.js +0 -182
  96. package/src/handlers/patterns/patternViolationPost.js +0 -185
  97. package/src/handlers/projects/projectCreate.js +0 -248
  98. package/src/handlers/projects/projectDelete.js +0 -82
  99. package/src/handlers/projects/projectGet.js +0 -95
  100. package/src/handlers/projects/projectUpdate.js +0 -117
  101. package/src/handlers/reports/aiLeverage.js +0 -210
  102. package/src/handlers/reports/engineeringInvestment.js +0 -132
  103. package/src/handlers/reports/riskForecast.js +0 -206
  104. package/src/handlers/reports/standardsRoi.js +0 -254
  105. package/src/handlers/scheduled/analyzeCorrelations.js +0 -178
  106. package/src/handlers/scheduled/analyzeGitHistory.js +0 -510
  107. package/src/handlers/scheduled/generateAlerts.js +0 -135
  108. package/src/handlers/scheduled/maturityUpdateJob.js +0 -166
  109. package/src/handlers/scheduled/refreshActivity.js +0 -21
  110. package/src/handlers/scheduled/scanCompliance.js +0 -334
  111. package/src/handlers/sessions/sessionEndPost.js +0 -180
  112. package/src/handlers/sessions/sessionStandardsPost.js +0 -171
  113. package/src/handlers/standards/catalogGet.js +0 -185
  114. package/src/handlers/standards/catalogSync.js +0 -120
  115. package/src/handlers/standards/discoveriesGet.js +0 -89
  116. package/src/handlers/standards/projectStandardsGet.js +0 -129
  117. package/src/handlers/standards/projectStandardsPut.js +0 -151
  118. package/src/handlers/standards/standardsAuditGet.js +0 -65
  119. package/src/handlers/standards/standardsParseUpload.js +0 -149
  120. package/src/handlers/standards/standardsRelevantPost.js +0 -405
  121. package/src/handlers/standards/standardsTransition.js +0 -161
  122. package/src/handlers/stripe/addonManagePost.js +0 -240
  123. package/src/handlers/stripe/billingPortalPost.js +0 -93
  124. package/src/handlers/stripe/enterpriseCheckoutPost.js +0 -272
  125. package/src/handlers/stripe/seatsUpdatePost.js +0 -185
  126. package/src/handlers/stripe/subscriptionCancelDelete.js +0 -169
  127. package/src/handlers/stripe/subscriptionCreatePost.js +0 -221
  128. package/src/handlers/stripe/subscriptionUpdatePut.js +0 -163
  129. package/src/handlers/stripe/webhookPost.js +0 -482
  130. package/src/handlers/user/apiTokenCreate.js +0 -71
  131. package/src/handlers/user/apiTokenList.js +0 -64
  132. package/src/handlers/user/userSplashAck.js +0 -91
  133. package/src/handlers/user/userSplashGet.js +0 -211
  134. package/src/handlers/users/cognitoPostConfirmation.js +0 -186
  135. package/src/handlers/users/cognitoPreSignUp.js +0 -114
  136. package/src/handlers/users/userEntitlementsGet.js +0 -89
  137. package/src/handlers/users/userGet.js +0 -118
  138. package/src/handlers/users/userProfilePut.js +0 -77
  139. 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 };